khal-os 1.260324.13 → 1.260324.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "khal-os",
3
- "version": "1.260324.13",
3
+ "version": "1.260324.14",
4
4
  "scripts": {
5
5
  "dev": "next dev --port 8888",
6
6
  "build": "NEXT_PRIVATE_BUILD_WORKER=false next build",
@@ -58,14 +58,14 @@ export const agentLifecycleHandlers: ServiceHandler[] = [
58
58
  subject: SUBJECTS.agent.history(),
59
59
  handler: (msg) => {
60
60
  try {
61
- const { name, full, since } = msg.json<{ name: string; full?: boolean; since?: string }>();
61
+ const { name, full, since } = msg.json<{ name: string; full?: boolean; since?: number }>();
62
62
  if (!name) {
63
63
  msg.respond(JSON.stringify({ error: 'Missing required field: name' }));
64
64
  return;
65
65
  }
66
66
  const args = ['history', name, '--json'];
67
67
  if (full) args.push('--full');
68
- if (since) args.push('--since', since);
68
+ if (since !== undefined) args.push('--since', String(since));
69
69
 
70
70
  const result = runGenie<unknown[]>(args, { timeout: 15_000 });
71
71
  if (!result.ok) {
@@ -45,14 +45,14 @@ export const commsHandlers: ServiceHandler[] = [
45
45
  subject: SUBJECTS.comms.broadcast(),
46
46
  handler: (msg) => {
47
47
  try {
48
- const req = msg.json<{ body: string; team?: string }>();
48
+ const req = msg.json<{ body: string; from?: string }>();
49
49
  if (!req.body) {
50
50
  msg.respond(JSON.stringify({ error: 'Missing required field: body' }));
51
51
  return;
52
52
  }
53
53
 
54
54
  const args = ['broadcast', req.body];
55
- if (req.team) args.push('--team', req.team);
55
+ if (req.from) args.push('--from', req.from);
56
56
 
57
57
  const result = runGenie(args, { json: false });
58
58
  if (!result.ok) {
@@ -14,7 +14,9 @@ interface DirAddPayload {
14
14
  dir?: string;
15
15
  repo?: string;
16
16
  model?: string;
17
+ promptMode?: string;
17
18
  roles?: string[];
19
+ global?: boolean;
18
20
  }
19
21
 
20
22
  interface DirRemovePayload {
@@ -26,8 +28,9 @@ interface DirEditPayload {
26
28
  model?: string;
27
29
  dir?: string;
28
30
  repo?: string;
31
+ promptMode?: string;
29
32
  roles?: string[];
30
- description?: string;
33
+ global?: boolean;
31
34
  }
32
35
 
33
36
  export const directorySubscriptions = [
@@ -80,7 +83,7 @@ export const directorySubscriptions = [
80
83
  subject: SUBJECTS.dir.add(),
81
84
  handler: (msg: { data: Uint8Array; json: <T>() => T; respond: (data: string) => void }) => {
82
85
  try {
83
- const { name, dir, repo, model, roles } = msg.json<DirAddPayload>();
86
+ const { name, dir, repo, model, promptMode, roles, global: isGlobal } = msg.json<DirAddPayload>();
84
87
  if (!name) {
85
88
  msg.respond(JSON.stringify({ error: 'name is required' }));
86
89
  return;
@@ -90,11 +93,9 @@ export const directorySubscriptions = [
90
93
  if (dir) args.push('--dir', dir);
91
94
  if (repo) args.push('--repo', repo);
92
95
  if (model) args.push('--model', model);
93
- if (roles) {
94
- for (const role of roles) {
95
- args.push('--role', role);
96
- }
97
- }
96
+ if (promptMode) args.push('--prompt-mode', promptMode);
97
+ if (roles?.length) args.push('--roles', ...roles);
98
+ if (isGlobal) args.push('--global');
98
99
 
99
100
  const result = runGenie(args, { json: false });
100
101
  if (!result.ok) {
@@ -136,7 +137,7 @@ export const directorySubscriptions = [
136
137
  subject: SUBJECTS.dir.edit(),
137
138
  handler: (msg: { data: Uint8Array; json: <T>() => T; respond: (data: string) => void }) => {
138
139
  try {
139
- const { name, model, dir, repo, roles, description } = msg.json<DirEditPayload>();
140
+ const { name, model, dir, repo, promptMode, roles, global: isGlobal } = msg.json<DirEditPayload>();
140
141
  if (!name) {
141
142
  msg.respond(JSON.stringify({ error: 'name is required' }));
142
143
  return;
@@ -146,12 +147,9 @@ export const directorySubscriptions = [
146
147
  if (model) args.push('--model', model);
147
148
  if (dir) args.push('--dir', dir);
148
149
  if (repo) args.push('--repo', repo);
149
- if (description) args.push('--description', description);
150
- if (roles) {
151
- for (const role of roles) {
152
- args.push('--role', role);
153
- }
154
- }
150
+ if (promptMode) args.push('--prompt-mode', promptMode);
151
+ if (roles?.length) args.push('--roles', ...roles);
152
+ if (isGlobal) args.push('--global');
155
153
 
156
154
  const result = runGenie(args, { json: false });
157
155
  if (!result.ok) {
@@ -2,6 +2,7 @@ import type { NatsConnection } from '@khal-os/sdk/service';
2
2
  import { createService } from '@khal-os/sdk/service';
3
3
  import { agentLifecycleHandlers } from './agent-lifecycle';
4
4
  import { appsHandlers } from './apps';
5
+ import { runGenieAsync } from './cli';
5
6
  import { commsHandlers } from './comms';
6
7
  import { directorySubscriptions } from './directory';
7
8
  import { systemSubscriptions } from './system';
@@ -133,18 +134,49 @@ createService({
133
134
  // --- Spawn agent ---
134
135
  {
135
136
  subject: 'os.genie.spawn',
136
- handler: (msg) => {
137
+ handler: async (msg) => {
137
138
  try {
138
- const { execSync } = require('node:child_process');
139
- const req = msg.json<{ role: string; team?: string; repo?: string }>();
140
- const args = [req.role];
139
+ const req = msg.json<{
140
+ role: string;
141
+ team?: string;
142
+ repo?: string;
143
+ provider?: string;
144
+ model?: string;
145
+ skill?: string;
146
+ layout?: string;
147
+ color?: string;
148
+ planMode?: boolean;
149
+ permissionMode?: string;
150
+ cwd?: string;
151
+ session?: string;
152
+ }>();
153
+ if (!req.role) {
154
+ msg.respond(JSON.stringify({ ok: false, error: 'Missing required field: role' }));
155
+ return;
156
+ }
157
+
158
+ const args = ['spawn', req.role];
141
159
  if (req.team) args.push('--team', req.team);
142
- const result = execSync(`genie spawn ${args.join(' ')}`, {
160
+ if (req.provider) args.push('--provider', req.provider);
161
+ if (req.model) args.push('--model', req.model);
162
+ if (req.skill) args.push('--skill', req.skill);
163
+ if (req.layout) args.push('--layout', req.layout);
164
+ if (req.color) args.push('--color', req.color);
165
+ if (req.planMode) args.push('--plan-mode');
166
+ if (req.permissionMode) args.push('--permission-mode', req.permissionMode);
167
+ if (req.cwd) args.push('--cwd', req.cwd);
168
+ if (req.session) args.push('--session', req.session);
169
+
170
+ const result = await runGenieAsync(args, {
171
+ json: false,
172
+ timeout: 15_000,
143
173
  cwd: req.repo || process.env.HOME,
144
- timeout: 15000,
145
- encoding: 'utf-8',
146
174
  });
147
- msg.respond(JSON.stringify({ ok: true, output: result.trim() }));
175
+ if (!result.ok) {
176
+ msg.respond(JSON.stringify({ ok: false, error: result.error }));
177
+ return;
178
+ }
179
+ msg.respond(JSON.stringify({ ok: true, output: result.data }));
148
180
  } catch (err) {
149
181
  msg.respond(JSON.stringify({ ok: false, error: String(err) }));
150
182
  }
@@ -57,7 +57,7 @@ export const teamsHandlers: ServiceHandler[] = [
57
57
  subject: SUBJECTS.teams.create(),
58
58
  handler: async (msg) => {
59
59
  try {
60
- const req = msg.json<{ name: string; repo: string; branch?: string; wish?: string }>();
60
+ const req = msg.json<{ name: string; repo: string; branch?: string; wish?: string; session?: string }>();
61
61
  if (!req.name || !req.repo) {
62
62
  msg.respond(JSON.stringify({ ok: false, error: 'Missing required fields: name, repo' }));
63
63
  return;
@@ -66,6 +66,7 @@ export const teamsHandlers: ServiceHandler[] = [
66
66
  const args = ['team', 'create', req.name, '--repo', req.repo];
67
67
  if (req.branch) args.push('--branch', req.branch);
68
68
  if (req.wish) args.push('--wish', req.wish);
69
+ if (req.session) args.push('--session', req.session);
69
70
 
70
71
  const result = await runGenieAsync(args, { json: false, timeout: 120_000 });
71
72
  if (!result.ok) {
@@ -29,10 +29,16 @@ export function useNatsRequest<T = unknown>(
29
29
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
30
30
  const mountedRef = useRef(true);
31
31
 
32
+ // Stabilize payload by serializing — prevents infinite re-render loops
33
+ // when callers pass inline objects like { name: teamName }
34
+ const payloadKey = JSON.stringify(payload ?? null);
35
+ const payloadRef = useRef(payload);
36
+ payloadRef.current = payload;
37
+
32
38
  const fetchData = useCallback(async () => {
33
39
  try {
34
40
  const client = getNatsClient();
35
- const response = await client.request(subject, payload ?? {}, 5000);
41
+ const response = await client.request(subject, payloadRef.current ?? {}, 5000);
36
42
  if (!mountedRef.current) return;
37
43
  setData(response as T);
38
44
  setError(null);
@@ -44,7 +50,7 @@ export function useNatsRequest<T = unknown>(
44
50
  setLoading(false);
45
51
  }
46
52
  }
47
- }, [subject, payload]);
53
+ }, [subject, payloadKey]);
48
54
 
49
55
  useEffect(() => {
50
56
  mountedRef.current = true;