heyio 0.21.4 → 0.23.0
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/api/server.js +0 -25
- package/dist/auth/session-logic.js +79 -0
- package/dist/auth/session-logic.test.js +201 -0
- package/dist/copilot/system-message.js +8 -0
- package/dist/copilot/tools.js +30 -31
- package/dist/copilot/universes.js +150 -0
- package/dist/store/feed.test.js +73 -1
- package/dist/store/squads.js +2 -2
- package/package.json +3 -3
- package/web-dist/assets/{index-VjIh_XUc.js → index-CF9f3i0T.js} +28 -28
- package/web-dist/assets/index-CkG67otR.css +10 -0
- package/web-dist/index.html +2 -2
- package/src/releases.json +0 -72
- package/web-dist/assets/index-Bg0PZh6a.css +0 -10
package/dist/api/server.js
CHANGED
|
@@ -20,27 +20,6 @@ import { runScheduleNow } from "../copilot/scheduler.js";
|
|
|
20
20
|
import { runIoScheduleNow } from "../copilot/io-scheduler.js";
|
|
21
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
22
|
const WEB_DIST = path.resolve(__dirname, "../../web-dist");
|
|
23
|
-
let releasesCache = null;
|
|
24
|
-
function loadReleases() {
|
|
25
|
-
if (releasesCache)
|
|
26
|
-
return releasesCache;
|
|
27
|
-
// Check both dev (src/api/ → src/) and production (dist/api/ → src/) paths
|
|
28
|
-
const candidates = [
|
|
29
|
-
path.join(__dirname, "../releases.json"), // dist/api/ → dist/releases.json
|
|
30
|
-
path.join(__dirname, "../../src/releases.json"), // dist/api/ → src/releases.json
|
|
31
|
-
];
|
|
32
|
-
for (const candidate of candidates) {
|
|
33
|
-
try {
|
|
34
|
-
if (!existsSync(candidate))
|
|
35
|
-
continue;
|
|
36
|
-
const raw = readFileSync(candidate, "utf-8");
|
|
37
|
-
releasesCache = JSON.parse(raw);
|
|
38
|
-
return releasesCache;
|
|
39
|
-
}
|
|
40
|
-
catch { /* try next */ }
|
|
41
|
-
}
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
23
|
let messageHandler;
|
|
45
24
|
const sseConnections = new Set();
|
|
46
25
|
export function setMessageHandler(handler) {
|
|
@@ -173,10 +152,6 @@ export async function startApiServer() {
|
|
|
173
152
|
api.get("/status", (_req, res) => {
|
|
174
153
|
res.json({ version: IO_VERSION, uptime: process.uptime() });
|
|
175
154
|
});
|
|
176
|
-
// Releases endpoint — serves build-time bundled GitHub release notes
|
|
177
|
-
api.get("/releases", (_req, res) => {
|
|
178
|
-
res.json({ releases: loadReleases() });
|
|
179
|
-
});
|
|
180
155
|
// SSE events endpoint
|
|
181
156
|
api.get("/events", (req, res) => {
|
|
182
157
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure session-handling logic extracted from web/src/stores/auth.ts.
|
|
3
|
+
*
|
|
4
|
+
* These functions contain the core behavioral decisions made across four
|
|
5
|
+
* consecutive auth patches (#189→#192→#194→#196) that fixed undocumented
|
|
6
|
+
* Supabase JS v2 side-effects. They are expressed as framework-agnostic
|
|
7
|
+
* functions so they can be unit-tested with the Node test runner without
|
|
8
|
+
* requiring Vue or Pinia.
|
|
9
|
+
*
|
|
10
|
+
* The authoritative implementation lives in web/src/stores/auth.ts.
|
|
11
|
+
* Any change to the auth store's session logic MUST be reflected here and
|
|
12
|
+
* all tests in session-logic.test.ts must continue to pass.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Mirrors the onAuthStateChange handler in web/src/stores/auth.ts.
|
|
16
|
+
*
|
|
17
|
+
* Decision: only SIGNED_OUT clears the cached session. All other events that
|
|
18
|
+
* fire with a null session (TOKEN_REFRESHED failure, lock timeout, internal
|
|
19
|
+
* reconciliation) are intentionally ignored to prevent cache poisoning.
|
|
20
|
+
*
|
|
21
|
+
* See squad decision 2026-05-16 03:17:52 and issue #193.
|
|
22
|
+
*/
|
|
23
|
+
export function handleAuthStateChange(cachedSession, event, newSession) {
|
|
24
|
+
if (newSession) {
|
|
25
|
+
// Accept any valid session regardless of event type
|
|
26
|
+
return newSession;
|
|
27
|
+
}
|
|
28
|
+
if (event === "SIGNED_OUT") {
|
|
29
|
+
// Only explicit sign-out clears the cache
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// All other null-session events: leave the existing cache untouched
|
|
33
|
+
return cachedSession;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Mirrors getAccessToken() in web/src/stores/auth.ts.
|
|
37
|
+
*
|
|
38
|
+
* Decisions:
|
|
39
|
+
* - Reads from cached session — NEVER calls getSession() (Supabase JS v2
|
|
40
|
+
* side-effect: can fire onAuthStateChange(null) during reconciliation).
|
|
41
|
+
* - Proactively refreshes when within 30 seconds of expiry.
|
|
42
|
+
* - Falls back to refreshSession() if cache is null and auth is enabled.
|
|
43
|
+
* - Returns null when auth is disabled.
|
|
44
|
+
*
|
|
45
|
+
* See squad decisions 2026-05-16 02:23:38, 2026-05-16 03:16:46 and issues
|
|
46
|
+
* #191, #193.
|
|
47
|
+
*/
|
|
48
|
+
export async function getAccessToken(cachedSession, authEnabled, refresh, now = Date.now) {
|
|
49
|
+
if (cachedSession?.access_token) {
|
|
50
|
+
// Proactively refresh when within 30 seconds of expiry
|
|
51
|
+
if (cachedSession.expires_at !== undefined &&
|
|
52
|
+
cachedSession.expires_at * 1000 - now() < 30_000) {
|
|
53
|
+
try {
|
|
54
|
+
const fresh = await refresh();
|
|
55
|
+
if (fresh)
|
|
56
|
+
return fresh.access_token;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// fall through to cached token
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return cachedSession.access_token;
|
|
63
|
+
}
|
|
64
|
+
// No cached session — attempt recovery if auth is enabled.
|
|
65
|
+
// Handles the edge case where the cache was spuriously cleared but a
|
|
66
|
+
// valid refresh token still exists in storage.
|
|
67
|
+
if (authEnabled) {
|
|
68
|
+
try {
|
|
69
|
+
const fresh = await refresh();
|
|
70
|
+
if (fresh)
|
|
71
|
+
return fresh.access_token;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// fall through to null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=session-logic.js.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth session handling regression tests — Supabase JS v2 integration decisions.
|
|
3
|
+
*
|
|
4
|
+
* These tests guard the behavioral decisions made across four consecutive auth
|
|
5
|
+
* patches (#189→#192→#194→#196) that fixed undocumented Supabase JS v2 side-
|
|
6
|
+
* effects. They test the logic extracted into src/auth/session-logic.ts, which
|
|
7
|
+
* mirrors the implementation in web/src/stores/auth.ts.
|
|
8
|
+
*
|
|
9
|
+
* If auth store logic changes, update session-logic.ts to match AND verify
|
|
10
|
+
* these tests still pass.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { handleAuthStateChange, getAccessToken, } from "./session-logic.js";
|
|
15
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
16
|
+
function makeSession(overrides = {}) {
|
|
17
|
+
return {
|
|
18
|
+
access_token: "tok-abc",
|
|
19
|
+
expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
|
|
20
|
+
user: { id: "user-1" },
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const neverCalled = async () => {
|
|
25
|
+
throw new Error("refresh should not have been called");
|
|
26
|
+
};
|
|
27
|
+
// Use a fixed epoch (multiple of 1000ms) for deterministic boundary tests.
|
|
28
|
+
// T = 1_000_000_000_000ms (epoch seconds = 1_000_000_000)
|
|
29
|
+
const FIXED_NOW_MS = 1_000_000_000_000;
|
|
30
|
+
const FIXED_NOW_S = FIXED_NOW_MS / 1000; // 1_000_000_000
|
|
31
|
+
// ── onAuthStateChange session caching ────────────────────────────────────────
|
|
32
|
+
describe("handleAuthStateChange — onAuthStateChange caching rules", () => {
|
|
33
|
+
it("accepts a valid session on SIGNED_IN", () => {
|
|
34
|
+
const s = makeSession();
|
|
35
|
+
const result = handleAuthStateChange(null, "SIGNED_IN", s);
|
|
36
|
+
assert.deepEqual(result, s);
|
|
37
|
+
});
|
|
38
|
+
it("accepts a valid session on INITIAL_SESSION", () => {
|
|
39
|
+
const s = makeSession();
|
|
40
|
+
const result = handleAuthStateChange(null, "INITIAL_SESSION", s);
|
|
41
|
+
assert.deepEqual(result, s);
|
|
42
|
+
});
|
|
43
|
+
it("accepts a valid session on TOKEN_REFRESHED", () => {
|
|
44
|
+
const old = makeSession({ access_token: "tok-old" });
|
|
45
|
+
const fresh = makeSession({ access_token: "tok-new" });
|
|
46
|
+
const result = handleAuthStateChange(old, "TOKEN_REFRESHED", fresh);
|
|
47
|
+
assert.equal(result?.access_token, "tok-new");
|
|
48
|
+
});
|
|
49
|
+
// Critical regression: SIGNED_OUT is the ONLY event that clears the cache.
|
|
50
|
+
it("clears session on SIGNED_OUT with null session", () => {
|
|
51
|
+
const s = makeSession();
|
|
52
|
+
const result = handleAuthStateChange(s, "SIGNED_OUT", null);
|
|
53
|
+
assert.equal(result, null);
|
|
54
|
+
});
|
|
55
|
+
// Critical regression: internal null-session events must NOT poison the cache.
|
|
56
|
+
it("does NOT clear session on TOKEN_REFRESHED with null — keeps existing cache", () => {
|
|
57
|
+
const cached = makeSession({ access_token: "tok-valid" });
|
|
58
|
+
const result = handleAuthStateChange(cached, "TOKEN_REFRESHED", null);
|
|
59
|
+
assert.equal(result?.access_token, "tok-valid");
|
|
60
|
+
});
|
|
61
|
+
it("does NOT clear session on INITIAL_SESSION with null — keeps existing cache", () => {
|
|
62
|
+
const cached = makeSession({ access_token: "tok-valid" });
|
|
63
|
+
const result = handleAuthStateChange(cached, "INITIAL_SESSION", null);
|
|
64
|
+
assert.equal(result?.access_token, "tok-valid");
|
|
65
|
+
});
|
|
66
|
+
it("does NOT clear session on USER_UPDATED with null — keeps existing cache", () => {
|
|
67
|
+
const cached = makeSession({ access_token: "tok-valid" });
|
|
68
|
+
const result = handleAuthStateChange(cached, "USER_UPDATED", null);
|
|
69
|
+
assert.equal(result?.access_token, "tok-valid");
|
|
70
|
+
});
|
|
71
|
+
it("returns null when cache is already null and null-session event fires", () => {
|
|
72
|
+
const result = handleAuthStateChange(null, "TOKEN_REFRESHED", null);
|
|
73
|
+
assert.equal(result, null);
|
|
74
|
+
});
|
|
75
|
+
it("null-session SIGNED_OUT with no prior cache stays null", () => {
|
|
76
|
+
const result = handleAuthStateChange(null, "SIGNED_OUT", null);
|
|
77
|
+
assert.equal(result, null);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
// ── getAccessToken — cached session path ─────────────────────────────────────
|
|
81
|
+
describe("getAccessToken — reads from cached session", () => {
|
|
82
|
+
it("returns cached access token when session is valid and not near expiry", async () => {
|
|
83
|
+
const session = makeSession({ access_token: "tok-valid" });
|
|
84
|
+
const token = await getAccessToken(session, true, neverCalled);
|
|
85
|
+
assert.equal(token, "tok-valid");
|
|
86
|
+
});
|
|
87
|
+
it("returns null when auth is disabled and cache is null", async () => {
|
|
88
|
+
const token = await getAccessToken(null, false, neverCalled);
|
|
89
|
+
assert.equal(token, null);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
// ── getAccessToken — proactive refresh near expiry ───────────────────────────
|
|
93
|
+
describe("getAccessToken — proactive refresh within 30s of expiry", () => {
|
|
94
|
+
it("calls refresh and returns new token when within 30s of expiry", async () => {
|
|
95
|
+
// Session expires in 25 seconds (within 30s threshold)
|
|
96
|
+
const session = makeSession({
|
|
97
|
+
access_token: "tok-expiring",
|
|
98
|
+
expires_at: Math.floor(Date.now() / 1000) + 25,
|
|
99
|
+
});
|
|
100
|
+
const freshSession = makeSession({ access_token: "tok-fresh" });
|
|
101
|
+
let refreshCalled = false;
|
|
102
|
+
const refresh = async () => { refreshCalled = true; return freshSession; };
|
|
103
|
+
const token = await getAccessToken(session, true, refresh);
|
|
104
|
+
assert.equal(refreshCalled, true);
|
|
105
|
+
assert.equal(token, "tok-fresh");
|
|
106
|
+
});
|
|
107
|
+
it("does NOT call refresh when expiry is more than 30s away", async () => {
|
|
108
|
+
const session = makeSession({
|
|
109
|
+
access_token: "tok-valid",
|
|
110
|
+
expires_at: Math.floor(Date.now() / 1000) + 60, // 60s away
|
|
111
|
+
});
|
|
112
|
+
let refreshCalled = false;
|
|
113
|
+
const refresh = async () => { refreshCalled = true; return null; };
|
|
114
|
+
const token = await getAccessToken(session, true, refresh);
|
|
115
|
+
assert.equal(refreshCalled, false);
|
|
116
|
+
assert.equal(token, "tok-valid");
|
|
117
|
+
});
|
|
118
|
+
it("falls back to cached token if refresh throws near expiry", async () => {
|
|
119
|
+
const session = makeSession({
|
|
120
|
+
access_token: "tok-expiring",
|
|
121
|
+
expires_at: Math.floor(Date.now() / 1000) + 10,
|
|
122
|
+
});
|
|
123
|
+
const refresh = async () => { throw new Error("network error"); };
|
|
124
|
+
const token = await getAccessToken(session, true, refresh);
|
|
125
|
+
assert.equal(token, "tok-expiring");
|
|
126
|
+
});
|
|
127
|
+
it("falls back to cached token if refresh returns null near expiry", async () => {
|
|
128
|
+
const session = makeSession({
|
|
129
|
+
access_token: "tok-expiring",
|
|
130
|
+
expires_at: Math.floor(Date.now() / 1000) + 10,
|
|
131
|
+
});
|
|
132
|
+
const refresh = async () => null;
|
|
133
|
+
const token = await getAccessToken(session, true, refresh);
|
|
134
|
+
assert.equal(token, "tok-expiring");
|
|
135
|
+
});
|
|
136
|
+
// Boundary: expires_at * 1000 - now == 30_000 is NOT < 30_000, so no refresh.
|
|
137
|
+
// Use FIXED_NOW_MS (exact multiple of 1000) to avoid floor-truncation artifacts.
|
|
138
|
+
it("does NOT refresh when exactly 30s remain (boundary is exclusive)", async () => {
|
|
139
|
+
const session = makeSession({
|
|
140
|
+
access_token: "tok-boundary",
|
|
141
|
+
expires_at: FIXED_NOW_S + 30, // exactly 30_000ms away from FIXED_NOW_MS
|
|
142
|
+
});
|
|
143
|
+
let refreshCalled = false;
|
|
144
|
+
const refresh = async () => { refreshCalled = true; return null; };
|
|
145
|
+
const token = await getAccessToken(session, true, refresh, () => FIXED_NOW_MS);
|
|
146
|
+
assert.equal(refreshCalled, false);
|
|
147
|
+
assert.equal(token, "tok-boundary");
|
|
148
|
+
});
|
|
149
|
+
// Boundary: expires_at * 1000 - now == 29_000 is < 30_000, so refresh fires.
|
|
150
|
+
it("refreshes when 29s remain (1s inside the 30s window)", async () => {
|
|
151
|
+
const freshSession = makeSession({ access_token: "tok-refreshed" });
|
|
152
|
+
const session = makeSession({
|
|
153
|
+
access_token: "tok-boundary",
|
|
154
|
+
expires_at: FIXED_NOW_S + 29, // 29_000ms away — within threshold
|
|
155
|
+
});
|
|
156
|
+
let refreshCalled = false;
|
|
157
|
+
const refresh = async () => { refreshCalled = true; return freshSession; };
|
|
158
|
+
const token = await getAccessToken(session, true, refresh, () => FIXED_NOW_MS);
|
|
159
|
+
assert.equal(refreshCalled, true);
|
|
160
|
+
assert.equal(token, "tok-refreshed");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
// ── getAccessToken — recovery fallback when cache is null ────────────────────
|
|
164
|
+
describe("getAccessToken — recovery fallback when cache is null", () => {
|
|
165
|
+
it("attempts refreshSession() when cache is null and auth is enabled", async () => {
|
|
166
|
+
const freshSession = makeSession({ access_token: "tok-recovered" });
|
|
167
|
+
let refreshCalled = false;
|
|
168
|
+
const refresh = async () => { refreshCalled = true; return freshSession; };
|
|
169
|
+
const token = await getAccessToken(null, true, refresh);
|
|
170
|
+
assert.equal(refreshCalled, true);
|
|
171
|
+
assert.equal(token, "tok-recovered");
|
|
172
|
+
});
|
|
173
|
+
it("returns null when auth is enabled but refreshSession() returns null", async () => {
|
|
174
|
+
const token = await getAccessToken(null, true, async () => null);
|
|
175
|
+
assert.equal(token, null);
|
|
176
|
+
});
|
|
177
|
+
it("returns null when auth is enabled but refreshSession() throws", async () => {
|
|
178
|
+
const refresh = async () => { throw new Error("refresh failed"); };
|
|
179
|
+
const token = await getAccessToken(null, true, refresh);
|
|
180
|
+
assert.equal(token, null);
|
|
181
|
+
});
|
|
182
|
+
it("does NOT call refreshSession() when auth is disabled", async () => {
|
|
183
|
+
let refreshCalled = false;
|
|
184
|
+
const refresh = async () => { refreshCalled = true; return null; };
|
|
185
|
+
const token = await getAccessToken(null, false, refresh);
|
|
186
|
+
assert.equal(refreshCalled, false);
|
|
187
|
+
assert.equal(token, null);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// ── getAccessToken — session without expires_at ───────────────────────────────
|
|
191
|
+
describe("getAccessToken — session without expires_at", () => {
|
|
192
|
+
it("returns cached token without attempting refresh if expires_at is undefined", async () => {
|
|
193
|
+
const session = { access_token: "tok-no-expiry" };
|
|
194
|
+
let refreshCalled = false;
|
|
195
|
+
const refresh = async () => { refreshCalled = true; return null; };
|
|
196
|
+
const token = await getAccessToken(session, true, refresh);
|
|
197
|
+
assert.equal(refreshCalled, false);
|
|
198
|
+
assert.equal(token, "tok-no-expiry");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
//# sourceMappingURL=session-logic.test.js.map
|
|
@@ -100,6 +100,14 @@ When an agent finishes a task, the other squad members automatically review the
|
|
|
100
100
|
- When all QA approvals pass (or no QA agents exist) and the task result contains a GitHub PR URL, the PR is automatically promoted from draft to ready via \`gh pr ready\`.
|
|
101
101
|
- Use \`squad_task_reviews\` to inspect the reviews on any completed task.
|
|
102
102
|
|
|
103
|
+
#### GitHub Self-Review Limitation
|
|
104
|
+
All squad agents share the repo owner's \`gh\` CLI identity. **GitHub blocks self-review** — \`gh pr review <number> --approve\` silently produces no review record when the same account authored the PR. This affects all squad-authored PRs.
|
|
105
|
+
|
|
106
|
+
**Workaround — review comments as audit trail:**
|
|
107
|
+
- Veto-capable reviewers must use \`gh pr review <number> --comment --body "LGTM — approved by <agent name>. <review summary>"\` instead of \`--approve\`. This creates a visible PR comment even though it is not a formal GitHub approval.
|
|
108
|
+
- For formal GitHub approval (required by branch protection rules if enabled), Michael must approve the PR himself or a separate bot/collaborator account must be configured.
|
|
109
|
+
- **Merge criteria:** all veto-capable members have posted an approving review comment, AND CI passes (\`gh pr checks <number> --watch\`), AND no merge conflicts. The team lead verifies all comments are present before merging.
|
|
110
|
+
|
|
103
111
|
### Squad Build Checklist
|
|
104
112
|
After \`squad_create\`, before delegating real work:
|
|
105
113
|
1. Add domain-specialist agents with \`squad_add_agent\` (use roles tailored to the project's stack).
|
package/dist/copilot/tools.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineTool } from "@github/copilot-sdk";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { execSync } from "child_process";
|
|
3
|
+
import { execSync, execFileSync } from "child_process";
|
|
4
4
|
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs";
|
|
5
5
|
import { join, dirname, resolve } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { UNIVERSES } from "./universes.js";
|
|
7
|
+
import { UNIVERSES, getOrCreateUniverse } from "./universes.js";
|
|
8
8
|
import { createFeedEntry } from "../store/feed.js";
|
|
9
9
|
import { validateCron, nextRun } from "./cron.js";
|
|
10
10
|
import { createIoSchedule, deleteIoSchedule, getIoSchedule, listIoSchedules, setIoScheduleEnabled, updateIoScheduleNextRun, } from "../store/io-schedules.js";
|
|
@@ -294,15 +294,15 @@ export function createTools(deps) {
|
|
|
294
294
|
name: z.string().describe("Display name (e.g., 'IO Assistant')"),
|
|
295
295
|
project_path: z.string().describe("Path to the project directory"),
|
|
296
296
|
universe: z
|
|
297
|
-
.
|
|
297
|
+
.string()
|
|
298
298
|
.optional()
|
|
299
|
-
.describe("
|
|
299
|
+
.describe("Universe theme for agent characters. Built-in: a-team, transformers, thundercats, gi-joe, aliens, ghostbusters, tmnt, star-wars, star-trek, lord-of-the-rings, the-office, parks-and-rec. Or provide any custom name. Random if omitted."),
|
|
300
300
|
}),
|
|
301
301
|
handler: async ({ slug, name, project_path, universe }) => {
|
|
302
302
|
try {
|
|
303
303
|
deps.createSquad(slug, name, project_path, universe);
|
|
304
304
|
const squad = deps.getSquad(slug);
|
|
305
|
-
const universeName =
|
|
305
|
+
const universeName = squad?.universe ? getOrCreateUniverse(squad.universe).name : "random";
|
|
306
306
|
return `Squad "${name}" created for ${project_path}\nUniverse: ${universeName}\n\nNext steps:\n1. Use \`squad_analyze\` to examine the project\n2. Use \`squad_add_agent\` to add specialists based on the analysis`;
|
|
307
307
|
}
|
|
308
308
|
catch (err) {
|
|
@@ -1123,11 +1123,11 @@ export function createTools(deps) {
|
|
|
1123
1123
|
handler: async ({ pattern, path: searchPath, include }) => {
|
|
1124
1124
|
console.error(`[io] grep tool called: ${pattern} in ${searchPath || "."}`);
|
|
1125
1125
|
try {
|
|
1126
|
-
|
|
1126
|
+
const args = ["-rn", pattern];
|
|
1127
1127
|
if (include)
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const result =
|
|
1128
|
+
args.push(`--include=${include}`);
|
|
1129
|
+
args.push(searchPath || ".");
|
|
1130
|
+
const result = execFileSync("grep", args, {
|
|
1131
1131
|
encoding: "utf-8",
|
|
1132
1132
|
timeout: 30_000,
|
|
1133
1133
|
maxBuffer: 1024 * 1024,
|
|
@@ -1245,31 +1245,30 @@ export function createTools(deps) {
|
|
|
1245
1245
|
handler: async ({ action, repo, title, body, labels, assignees, number, base, head, state, limit, review_action }) => {
|
|
1246
1246
|
console.error(`[io] github tool called: ${action} on ${repo}`);
|
|
1247
1247
|
try {
|
|
1248
|
-
let
|
|
1249
|
-
const r = `--repo ${repo}`;
|
|
1248
|
+
let args;
|
|
1250
1249
|
switch (action) {
|
|
1251
1250
|
case "create_issue": {
|
|
1252
1251
|
if (!title)
|
|
1253
1252
|
return "Error: title is required for create_issue";
|
|
1254
|
-
|
|
1253
|
+
args = ["issue", "create", "--repo", repo, "--title", title];
|
|
1255
1254
|
if (body)
|
|
1256
|
-
|
|
1255
|
+
args.push("--body", body);
|
|
1257
1256
|
if (labels?.length)
|
|
1258
|
-
|
|
1257
|
+
args.push("--label", labels.join(","));
|
|
1259
1258
|
if (assignees?.length)
|
|
1260
|
-
|
|
1259
|
+
args.push("--assignee", assignees.join(","));
|
|
1261
1260
|
break;
|
|
1262
1261
|
}
|
|
1263
1262
|
case "list_issues": {
|
|
1264
|
-
|
|
1263
|
+
args = ["issue", "list", "--repo", repo, "--limit", String(limit ?? 10)];
|
|
1265
1264
|
if (state)
|
|
1266
|
-
|
|
1265
|
+
args.push("--state", state);
|
|
1267
1266
|
break;
|
|
1268
1267
|
}
|
|
1269
1268
|
case "view_issue": {
|
|
1270
1269
|
if (!number)
|
|
1271
1270
|
return "Error: number is required for view_issue";
|
|
1272
|
-
|
|
1271
|
+
args = ["issue", "view", String(number), "--repo", repo];
|
|
1273
1272
|
break;
|
|
1274
1273
|
}
|
|
1275
1274
|
case "comment_issue": {
|
|
@@ -1277,37 +1276,37 @@ export function createTools(deps) {
|
|
|
1277
1276
|
return "Error: number is required for comment_issue";
|
|
1278
1277
|
if (!body)
|
|
1279
1278
|
return "Error: body is required for comment_issue";
|
|
1280
|
-
|
|
1279
|
+
args = ["issue", "comment", String(number), "--repo", repo, "--body", body];
|
|
1281
1280
|
break;
|
|
1282
1281
|
}
|
|
1283
1282
|
case "close_issue": {
|
|
1284
1283
|
if (!number)
|
|
1285
1284
|
return "Error: number is required for close_issue";
|
|
1286
|
-
|
|
1285
|
+
args = ["issue", "close", String(number), "--repo", repo];
|
|
1287
1286
|
break;
|
|
1288
1287
|
}
|
|
1289
1288
|
case "create_pr": {
|
|
1290
1289
|
if (!title)
|
|
1291
1290
|
return "Error: title is required for create_pr";
|
|
1292
|
-
|
|
1291
|
+
args = ["pr", "create", "--repo", repo, "--title", title];
|
|
1293
1292
|
if (body)
|
|
1294
|
-
|
|
1293
|
+
args.push("--body", body);
|
|
1295
1294
|
if (base)
|
|
1296
|
-
|
|
1295
|
+
args.push("--base", base);
|
|
1297
1296
|
if (head)
|
|
1298
|
-
|
|
1297
|
+
args.push("--head", head);
|
|
1299
1298
|
break;
|
|
1300
1299
|
}
|
|
1301
1300
|
case "list_prs": {
|
|
1302
|
-
|
|
1301
|
+
args = ["pr", "list", "--repo", repo, "--limit", String(limit ?? 10)];
|
|
1303
1302
|
if (state)
|
|
1304
|
-
|
|
1303
|
+
args.push("--state", state);
|
|
1305
1304
|
break;
|
|
1306
1305
|
}
|
|
1307
1306
|
case "view_pr": {
|
|
1308
1307
|
if (!number)
|
|
1309
1308
|
return "Error: number is required for view_pr";
|
|
1310
|
-
|
|
1309
|
+
args = ["pr", "view", String(number), "--repo", repo];
|
|
1311
1310
|
break;
|
|
1312
1311
|
}
|
|
1313
1312
|
case "comment_pr": {
|
|
@@ -1315,7 +1314,7 @@ export function createTools(deps) {
|
|
|
1315
1314
|
return "Error: number is required for comment_pr";
|
|
1316
1315
|
if (!body)
|
|
1317
1316
|
return "Error: body is required for comment_pr";
|
|
1318
|
-
|
|
1317
|
+
args = ["pr", "comment", String(number), "--repo", repo, "--body", body];
|
|
1319
1318
|
break;
|
|
1320
1319
|
}
|
|
1321
1320
|
case "review_pr": {
|
|
@@ -1323,16 +1322,16 @@ export function createTools(deps) {
|
|
|
1323
1322
|
return "Error: number is required for review_pr";
|
|
1324
1323
|
if (!review_action)
|
|
1325
1324
|
return "Error: review_action is required for review_pr (approve, request-changes, or comment)";
|
|
1326
|
-
|
|
1325
|
+
args = ["pr", "review", String(number), "--repo", repo, `--${review_action}`];
|
|
1327
1326
|
if (body && (review_action === "request-changes" || review_action === "comment")) {
|
|
1328
|
-
|
|
1327
|
+
args.push("--body", body);
|
|
1329
1328
|
}
|
|
1330
1329
|
break;
|
|
1331
1330
|
}
|
|
1332
1331
|
default:
|
|
1333
1332
|
return `Unknown action: ${action}`;
|
|
1334
1333
|
}
|
|
1335
|
-
const result =
|
|
1334
|
+
const result = execFileSync("gh", args, {
|
|
1336
1335
|
encoding: "utf-8",
|
|
1337
1336
|
timeout: 30_000,
|
|
1338
1337
|
maxBuffer: 1024 * 1024,
|