gitclaw 0.3.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/README.md +440 -0
- package/dist/agents.d.ts +8 -0
- package/dist/agents.js +82 -0
- package/dist/audit.d.ts +27 -0
- package/dist/audit.js +55 -0
- package/dist/compliance.d.ts +30 -0
- package/dist/compliance.js +108 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +43 -0
- package/dist/examples.d.ts +6 -0
- package/dist/examples.js +40 -0
- package/dist/exports.d.ts +13 -0
- package/dist/exports.js +6 -0
- package/dist/hooks.d.ts +24 -0
- package/dist/hooks.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +542 -0
- package/dist/knowledge.d.ts +17 -0
- package/dist/knowledge.js +55 -0
- package/dist/loader.d.ts +64 -0
- package/dist/loader.js +222 -0
- package/dist/sandbox.d.ts +28 -0
- package/dist/sandbox.js +54 -0
- package/dist/sdk-hooks.d.ts +8 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/sdk-types.d.ts +127 -0
- package/dist/sdk-types.js +1 -0
- package/dist/sdk.d.ts +6 -0
- package/dist/sdk.js +444 -0
- package/dist/session.d.ts +15 -0
- package/dist/session.js +127 -0
- package/dist/skills.d.ts +18 -0
- package/dist/skills.js +104 -0
- package/dist/tool-loader.d.ts +3 -0
- package/dist/tool-loader.js +138 -0
- package/dist/tools/cli.d.ts +3 -0
- package/dist/tools/cli.js +86 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +29 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.js +128 -0
- package/dist/tools/read.d.ts +3 -0
- package/dist/tools/read.js +46 -0
- package/dist/tools/sandbox-cli.d.ts +4 -0
- package/dist/tools/sandbox-cli.js +48 -0
- package/dist/tools/sandbox-memory.d.ts +4 -0
- package/dist/tools/sandbox-memory.js +117 -0
- package/dist/tools/sandbox-read.d.ts +4 -0
- package/dist/tools/sandbox-read.js +25 -0
- package/dist/tools/sandbox-write.d.ts +4 -0
- package/dist/tools/sandbox-write.js +26 -0
- package/dist/tools/shared.d.ts +38 -0
- package/dist/tools/shared.js +69 -0
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.js +28 -0
- package/dist/workflows.d.ts +8 -0
- package/dist/workflows.js +81 -0
- package/package.json +57 -0
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import { loadAgent } from "./loader.js";
|
|
3
|
+
import { createBuiltinTools } from "./tools/index.js";
|
|
4
|
+
import { createSandboxContext } from "./sandbox.js";
|
|
5
|
+
import { loadHooksConfig, runHooks, wrapToolWithHooks } from "./hooks.js";
|
|
6
|
+
import { loadDeclarativeTools } from "./tool-loader.js";
|
|
7
|
+
import { buildTypeboxSchema } from "./tool-loader.js";
|
|
8
|
+
import { wrapToolWithProgrammaticHooks } from "./sdk-hooks.js";
|
|
9
|
+
import { initLocalSession } from "./session.js";
|
|
10
|
+
function createChannel() {
|
|
11
|
+
const buffer = [];
|
|
12
|
+
let resolve = null;
|
|
13
|
+
let done = false;
|
|
14
|
+
return {
|
|
15
|
+
push(v) {
|
|
16
|
+
if (resolve) {
|
|
17
|
+
resolve({ value: v, done: false });
|
|
18
|
+
resolve = null;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
buffer.push(v);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
finish() {
|
|
25
|
+
done = true;
|
|
26
|
+
if (resolve) {
|
|
27
|
+
resolve({ value: undefined, done: true });
|
|
28
|
+
resolve = null;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
pull() {
|
|
32
|
+
if (buffer.length) {
|
|
33
|
+
return Promise.resolve({ value: buffer.shift(), done: false });
|
|
34
|
+
}
|
|
35
|
+
if (done) {
|
|
36
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
37
|
+
}
|
|
38
|
+
return new Promise((r) => { resolve = r; });
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ── Convert GCToolDefinition → AgentTool ───────────────────────────────
|
|
43
|
+
function toAgentTool(def) {
|
|
44
|
+
const schema = buildTypeboxSchema(def.inputSchema);
|
|
45
|
+
return {
|
|
46
|
+
name: def.name,
|
|
47
|
+
label: def.name,
|
|
48
|
+
description: def.description,
|
|
49
|
+
parameters: schema,
|
|
50
|
+
execute: async (_toolCallId, params, signal) => {
|
|
51
|
+
const result = await def.handler(params, signal);
|
|
52
|
+
const text = typeof result === "string" ? result : result.text;
|
|
53
|
+
const details = typeof result === "object" && "details" in result
|
|
54
|
+
? result.details
|
|
55
|
+
: undefined;
|
|
56
|
+
return { content: [{ type: "text", text }], details };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ── Extract text/thinking from AssistantMessage ────────────────────────
|
|
61
|
+
function extractContent(msg) {
|
|
62
|
+
let text = "";
|
|
63
|
+
let thinking = "";
|
|
64
|
+
for (const block of msg.content) {
|
|
65
|
+
if (block.type === "text")
|
|
66
|
+
text += block.text;
|
|
67
|
+
if (block.type === "thinking")
|
|
68
|
+
thinking += block.thinking;
|
|
69
|
+
}
|
|
70
|
+
return { text, thinking };
|
|
71
|
+
}
|
|
72
|
+
// ── query() ────────────────────────────────────────────────────────────
|
|
73
|
+
export function query(options) {
|
|
74
|
+
const channel = createChannel();
|
|
75
|
+
const collectedMessages = [];
|
|
76
|
+
const ac = options.abortController ?? new AbortController();
|
|
77
|
+
// These are set once the agent is loaded (async init below)
|
|
78
|
+
let _sessionId = options.sessionId ?? "";
|
|
79
|
+
let _manifest = null;
|
|
80
|
+
// Accumulate streaming deltas for the current message
|
|
81
|
+
let accText = "";
|
|
82
|
+
let accThinking = "";
|
|
83
|
+
function pushMsg(msg) {
|
|
84
|
+
collectedMessages.push(msg);
|
|
85
|
+
channel.push(msg);
|
|
86
|
+
}
|
|
87
|
+
// Sandbox context (hoisted for cleanup in catch)
|
|
88
|
+
let sandboxCtx;
|
|
89
|
+
// Local session (hoisted for cleanup in catch)
|
|
90
|
+
let localSession;
|
|
91
|
+
// Async initialization + run
|
|
92
|
+
const runPromise = (async () => {
|
|
93
|
+
// Validate mutually exclusive options
|
|
94
|
+
if (options.repo && options.sandbox) {
|
|
95
|
+
throw new Error("repo and sandbox options are mutually exclusive");
|
|
96
|
+
}
|
|
97
|
+
let dir = options.dir ?? process.cwd();
|
|
98
|
+
// Local repo mode
|
|
99
|
+
if (options.repo) {
|
|
100
|
+
const token = options.repo.token || process.env.GITHUB_TOKEN || process.env.GIT_TOKEN;
|
|
101
|
+
if (!token) {
|
|
102
|
+
throw new Error("repo.token, GITHUB_TOKEN, or GIT_TOKEN is required with repo option");
|
|
103
|
+
}
|
|
104
|
+
localSession = initLocalSession({
|
|
105
|
+
url: options.repo.url,
|
|
106
|
+
token,
|
|
107
|
+
dir: options.repo.dir || dir,
|
|
108
|
+
session: options.repo.session,
|
|
109
|
+
});
|
|
110
|
+
dir = localSession.dir;
|
|
111
|
+
}
|
|
112
|
+
// 1. Load agent
|
|
113
|
+
const loaded = await loadAgent(dir, options.model, options.env);
|
|
114
|
+
_manifest = loaded.manifest;
|
|
115
|
+
_sessionId = _sessionId || loaded.sessionId;
|
|
116
|
+
// 2. Apply system prompt overrides
|
|
117
|
+
let systemPrompt = loaded.systemPrompt;
|
|
118
|
+
if (options.systemPrompt !== undefined) {
|
|
119
|
+
systemPrompt = options.systemPrompt;
|
|
120
|
+
}
|
|
121
|
+
if (options.systemPromptSuffix) {
|
|
122
|
+
systemPrompt += "\n\n" + options.systemPromptSuffix;
|
|
123
|
+
}
|
|
124
|
+
// 3. Build tools (with optional sandbox)
|
|
125
|
+
if (options.sandbox) {
|
|
126
|
+
const sandboxConfig = options.sandbox === true
|
|
127
|
+
? { provider: "e2b" }
|
|
128
|
+
: options.sandbox;
|
|
129
|
+
sandboxCtx = await createSandboxContext(sandboxConfig, dir);
|
|
130
|
+
await sandboxCtx.gitMachine.start();
|
|
131
|
+
}
|
|
132
|
+
let tools = [];
|
|
133
|
+
if (!options.replaceBuiltinTools) {
|
|
134
|
+
tools = createBuiltinTools({
|
|
135
|
+
dir,
|
|
136
|
+
timeout: loaded.manifest.runtime.timeout,
|
|
137
|
+
sandbox: sandboxCtx,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Declarative tools from tools/*.yaml
|
|
141
|
+
const declarativeTools = await loadDeclarativeTools(loaded.agentDir);
|
|
142
|
+
tools = [...tools, ...declarativeTools];
|
|
143
|
+
// SDK-provided tools
|
|
144
|
+
if (options.tools) {
|
|
145
|
+
tools = [...tools, ...options.tools.map(toAgentTool)];
|
|
146
|
+
}
|
|
147
|
+
// Filter by allowlist/denylist
|
|
148
|
+
if (options.allowedTools) {
|
|
149
|
+
const allowed = new Set(options.allowedTools);
|
|
150
|
+
tools = tools.filter((t) => allowed.has(t.name));
|
|
151
|
+
}
|
|
152
|
+
if (options.disallowedTools) {
|
|
153
|
+
const denied = new Set(options.disallowedTools);
|
|
154
|
+
tools = tools.filter((t) => !denied.has(t.name));
|
|
155
|
+
}
|
|
156
|
+
// 4. Wrap with script-based hooks
|
|
157
|
+
const hooksConfig = await loadHooksConfig(loaded.agentDir);
|
|
158
|
+
if (hooksConfig) {
|
|
159
|
+
tools = tools.map((t) => wrapToolWithHooks(t, hooksConfig, loaded.agentDir, _sessionId));
|
|
160
|
+
}
|
|
161
|
+
// 5. Wrap with programmatic hooks
|
|
162
|
+
if (options.hooks) {
|
|
163
|
+
tools = tools.map((t) => wrapToolWithProgrammaticHooks(t, options.hooks, _sessionId, loaded.manifest.name));
|
|
164
|
+
}
|
|
165
|
+
// 6. Run on_session_start hooks (script-based)
|
|
166
|
+
if (hooksConfig?.hooks.on_session_start) {
|
|
167
|
+
const result = await runHooks(hooksConfig.hooks.on_session_start, loaded.agentDir, {
|
|
168
|
+
event: "on_session_start",
|
|
169
|
+
session_id: _sessionId,
|
|
170
|
+
agent: loaded.manifest.name,
|
|
171
|
+
});
|
|
172
|
+
if (result.action === "block") {
|
|
173
|
+
pushMsg({
|
|
174
|
+
type: "system",
|
|
175
|
+
subtype: "hook_blocked",
|
|
176
|
+
content: `Session blocked by hook: ${result.reason || "no reason given"}`,
|
|
177
|
+
});
|
|
178
|
+
channel.finish();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 6b. Run on_session_start programmatic hook
|
|
183
|
+
if (options.hooks?.onSessionStart) {
|
|
184
|
+
const ctx = {
|
|
185
|
+
sessionId: _sessionId,
|
|
186
|
+
agentName: loaded.manifest.name,
|
|
187
|
+
event: "SessionStart",
|
|
188
|
+
};
|
|
189
|
+
const result = await options.hooks.onSessionStart(ctx);
|
|
190
|
+
if (result.action === "block") {
|
|
191
|
+
pushMsg({
|
|
192
|
+
type: "system",
|
|
193
|
+
subtype: "hook_blocked",
|
|
194
|
+
content: `Session blocked by hook: ${result.reason || "no reason given"}`,
|
|
195
|
+
});
|
|
196
|
+
channel.finish();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// 7. Build model options from constraints
|
|
201
|
+
const modelOptions = {};
|
|
202
|
+
const constraints = options.constraints ?? loaded.manifest.model.constraints;
|
|
203
|
+
if (constraints) {
|
|
204
|
+
const c = constraints;
|
|
205
|
+
if (c.temperature !== undefined)
|
|
206
|
+
modelOptions.temperature = c.temperature;
|
|
207
|
+
if (c.maxTokens !== undefined)
|
|
208
|
+
modelOptions.maxTokens = c.maxTokens;
|
|
209
|
+
if (c.max_tokens !== undefined)
|
|
210
|
+
modelOptions.maxTokens = c.max_tokens;
|
|
211
|
+
if (c.topP !== undefined)
|
|
212
|
+
modelOptions.topP = c.topP;
|
|
213
|
+
if (c.top_p !== undefined)
|
|
214
|
+
modelOptions.topP = c.top_p;
|
|
215
|
+
if (c.topK !== undefined)
|
|
216
|
+
modelOptions.topK = c.topK;
|
|
217
|
+
if (c.top_k !== undefined)
|
|
218
|
+
modelOptions.topK = c.top_k;
|
|
219
|
+
}
|
|
220
|
+
if (options.maxTurns !== undefined) {
|
|
221
|
+
modelOptions.maxTurns = options.maxTurns;
|
|
222
|
+
}
|
|
223
|
+
// 8. Create Agent
|
|
224
|
+
const agent = new Agent({
|
|
225
|
+
initialState: {
|
|
226
|
+
systemPrompt,
|
|
227
|
+
model: loaded.model,
|
|
228
|
+
tools,
|
|
229
|
+
...modelOptions,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
// 9. Subscribe to events and map to GCMessage
|
|
233
|
+
agent.subscribe((event) => {
|
|
234
|
+
switch (event.type) {
|
|
235
|
+
case "agent_start":
|
|
236
|
+
pushMsg({
|
|
237
|
+
type: "system",
|
|
238
|
+
subtype: "session_start",
|
|
239
|
+
content: `Agent ${loaded.manifest.name} started`,
|
|
240
|
+
metadata: { sessionId: _sessionId },
|
|
241
|
+
});
|
|
242
|
+
break;
|
|
243
|
+
case "message_update": {
|
|
244
|
+
const e = event.assistantMessageEvent;
|
|
245
|
+
if (e.type === "text_delta") {
|
|
246
|
+
accText += e.delta;
|
|
247
|
+
pushMsg({
|
|
248
|
+
type: "delta",
|
|
249
|
+
deltaType: "text",
|
|
250
|
+
content: e.delta,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else if (e.type === "thinking_delta") {
|
|
254
|
+
accThinking += e.delta;
|
|
255
|
+
pushMsg({
|
|
256
|
+
type: "delta",
|
|
257
|
+
deltaType: "thinking",
|
|
258
|
+
content: e.delta,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "message_end": {
|
|
264
|
+
// Only process assistant messages — skip user/toolResult
|
|
265
|
+
const raw = event.message;
|
|
266
|
+
if (!raw || raw.role !== "assistant")
|
|
267
|
+
break;
|
|
268
|
+
const msg = raw;
|
|
269
|
+
// Emit error system message if the LLM call failed
|
|
270
|
+
if (msg.stopReason === "error") {
|
|
271
|
+
pushMsg({
|
|
272
|
+
type: "system",
|
|
273
|
+
subtype: "error",
|
|
274
|
+
content: msg.errorMessage || "LLM request failed (unknown error)",
|
|
275
|
+
metadata: {
|
|
276
|
+
model: msg.model,
|
|
277
|
+
provider: msg.provider,
|
|
278
|
+
api: msg.api,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
// Still emit the assistant message so callers can inspect stopReason
|
|
282
|
+
}
|
|
283
|
+
const { text, thinking } = extractContent(msg);
|
|
284
|
+
const assistantMsg = {
|
|
285
|
+
type: "assistant",
|
|
286
|
+
content: text || accText,
|
|
287
|
+
thinking: (thinking || accThinking) || undefined,
|
|
288
|
+
model: msg.model ?? "unknown",
|
|
289
|
+
provider: msg.provider ?? "unknown",
|
|
290
|
+
stopReason: msg.stopReason ?? "stop",
|
|
291
|
+
errorMessage: msg.errorMessage,
|
|
292
|
+
usage: msg.usage ? {
|
|
293
|
+
inputTokens: msg.usage.input ?? 0,
|
|
294
|
+
outputTokens: msg.usage.output ?? 0,
|
|
295
|
+
cacheReadTokens: msg.usage.cacheRead ?? 0,
|
|
296
|
+
cacheWriteTokens: msg.usage.cacheWrite ?? 0,
|
|
297
|
+
totalTokens: msg.usage.totalTokens ?? 0,
|
|
298
|
+
costUsd: msg.usage.cost?.total ?? 0,
|
|
299
|
+
} : undefined,
|
|
300
|
+
};
|
|
301
|
+
pushMsg(assistantMsg);
|
|
302
|
+
// Reset accumulators
|
|
303
|
+
accText = "";
|
|
304
|
+
accThinking = "";
|
|
305
|
+
// Fire post_response hooks (non-blocking)
|
|
306
|
+
if (hooksConfig?.hooks.post_response) {
|
|
307
|
+
runHooks(hooksConfig.hooks.post_response, loaded.agentDir, {
|
|
308
|
+
event: "post_response",
|
|
309
|
+
session_id: _sessionId,
|
|
310
|
+
}).catch(() => { });
|
|
311
|
+
}
|
|
312
|
+
if (options.hooks?.postResponse) {
|
|
313
|
+
Promise.resolve(options.hooks.postResponse({
|
|
314
|
+
sessionId: _sessionId,
|
|
315
|
+
agentName: loaded.manifest.name,
|
|
316
|
+
event: "PostResponse",
|
|
317
|
+
})).catch(() => { });
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "tool_execution_start":
|
|
322
|
+
pushMsg({
|
|
323
|
+
type: "tool_use",
|
|
324
|
+
toolCallId: event.toolCallId,
|
|
325
|
+
toolName: event.toolName,
|
|
326
|
+
args: event.args ?? {},
|
|
327
|
+
});
|
|
328
|
+
break;
|
|
329
|
+
case "tool_execution_end": {
|
|
330
|
+
const text = event.result?.content?.[0]?.text ?? "";
|
|
331
|
+
pushMsg({
|
|
332
|
+
type: "tool_result",
|
|
333
|
+
toolCallId: event.toolCallId,
|
|
334
|
+
toolName: event.toolName,
|
|
335
|
+
content: text,
|
|
336
|
+
isError: event.isError,
|
|
337
|
+
});
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case "agent_end":
|
|
341
|
+
pushMsg({
|
|
342
|
+
type: "system",
|
|
343
|
+
subtype: "session_end",
|
|
344
|
+
content: `Agent ${loaded.manifest.name} finished`,
|
|
345
|
+
metadata: { sessionId: _sessionId },
|
|
346
|
+
});
|
|
347
|
+
channel.finish();
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
// 10. Send prompt
|
|
352
|
+
if (typeof options.prompt === "string") {
|
|
353
|
+
await agent.prompt(options.prompt);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// Multi-turn: iterate the async iterable
|
|
357
|
+
for await (const userMsg of options.prompt) {
|
|
358
|
+
pushMsg({ type: "user", content: userMsg.content });
|
|
359
|
+
await agent.prompt(userMsg.content);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Finalize local session if active
|
|
363
|
+
if (localSession) {
|
|
364
|
+
try {
|
|
365
|
+
localSession.finalize();
|
|
366
|
+
}
|
|
367
|
+
catch { /* best-effort */ }
|
|
368
|
+
}
|
|
369
|
+
// Stop sandbox if active
|
|
370
|
+
if (sandboxCtx) {
|
|
371
|
+
await sandboxCtx.gitMachine.stop().catch(() => { });
|
|
372
|
+
}
|
|
373
|
+
// Ensure channel finishes even if no agent_end event
|
|
374
|
+
channel.finish();
|
|
375
|
+
})().catch(async (err) => {
|
|
376
|
+
// Finalize local session on error
|
|
377
|
+
if (localSession) {
|
|
378
|
+
try {
|
|
379
|
+
localSession.finalize();
|
|
380
|
+
}
|
|
381
|
+
catch { /* best-effort */ }
|
|
382
|
+
}
|
|
383
|
+
// Stop sandbox on error
|
|
384
|
+
if (sandboxCtx) {
|
|
385
|
+
await sandboxCtx.gitMachine.stop().catch(() => { });
|
|
386
|
+
}
|
|
387
|
+
// Fire on_error hooks
|
|
388
|
+
if (options.hooks?.onError) {
|
|
389
|
+
Promise.resolve(options.hooks.onError({
|
|
390
|
+
sessionId: _sessionId,
|
|
391
|
+
agentName: _manifest?.name ?? "unknown",
|
|
392
|
+
event: "OnError",
|
|
393
|
+
error: err.message,
|
|
394
|
+
})).catch(() => { });
|
|
395
|
+
}
|
|
396
|
+
pushMsg({
|
|
397
|
+
type: "system",
|
|
398
|
+
subtype: "error",
|
|
399
|
+
content: err.message,
|
|
400
|
+
});
|
|
401
|
+
channel.finish();
|
|
402
|
+
});
|
|
403
|
+
// Build the Query object (AsyncGenerator + helpers)
|
|
404
|
+
const generator = {
|
|
405
|
+
abort() {
|
|
406
|
+
ac.abort();
|
|
407
|
+
},
|
|
408
|
+
steer(_message) {
|
|
409
|
+
// Steering requires agent reference — for now this is a placeholder.
|
|
410
|
+
// Full steering support would require exposing the Agent instance.
|
|
411
|
+
},
|
|
412
|
+
sessionId() {
|
|
413
|
+
return _sessionId;
|
|
414
|
+
},
|
|
415
|
+
manifest() {
|
|
416
|
+
if (!_manifest)
|
|
417
|
+
throw new Error("Agent not yet loaded");
|
|
418
|
+
return _manifest;
|
|
419
|
+
},
|
|
420
|
+
messages() {
|
|
421
|
+
return [...collectedMessages];
|
|
422
|
+
},
|
|
423
|
+
// AsyncGenerator protocol
|
|
424
|
+
next() {
|
|
425
|
+
return channel.pull();
|
|
426
|
+
},
|
|
427
|
+
return(value) {
|
|
428
|
+
channel.finish();
|
|
429
|
+
return Promise.resolve({ value, done: true });
|
|
430
|
+
},
|
|
431
|
+
throw(err) {
|
|
432
|
+
channel.finish();
|
|
433
|
+
return Promise.reject(err);
|
|
434
|
+
},
|
|
435
|
+
[Symbol.asyncIterator]() {
|
|
436
|
+
return generator;
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
return generator;
|
|
440
|
+
}
|
|
441
|
+
// ── tool() helper ──────────────────────────────────────────────────────
|
|
442
|
+
export function tool(name, description, inputSchema, handler) {
|
|
443
|
+
return { name, description, inputSchema, handler };
|
|
444
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface LocalRepoOptions {
|
|
2
|
+
url: string;
|
|
3
|
+
token: string;
|
|
4
|
+
dir: string;
|
|
5
|
+
session?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LocalSession {
|
|
8
|
+
dir: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
commitChanges(msg?: string): void;
|
|
12
|
+
push(): void;
|
|
13
|
+
finalize(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function initLocalSession(opts: LocalRepoOptions): LocalSession;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
6
|
+
function authedUrl(url, token) {
|
|
7
|
+
// https://github.com/org/repo → https://<token>@github.com/org/repo
|
|
8
|
+
return url.replace(/^https:\/\//, `https://${token}@`);
|
|
9
|
+
}
|
|
10
|
+
function cleanUrl(url) {
|
|
11
|
+
return url.replace(/^https:\/\/[^@]+@/, "https://");
|
|
12
|
+
}
|
|
13
|
+
function git(args, cwd) {
|
|
14
|
+
return execSync(`git ${args}`, { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
15
|
+
}
|
|
16
|
+
function getDefaultBranch(cwd) {
|
|
17
|
+
try {
|
|
18
|
+
// e.g. "origin/main" → "main"
|
|
19
|
+
const ref = git("symbolic-ref refs/remotes/origin/HEAD", cwd);
|
|
20
|
+
return ref.replace("refs/remotes/origin/", "");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Fallback: try main, then master
|
|
24
|
+
try {
|
|
25
|
+
git("rev-parse --verify origin/main", cwd);
|
|
26
|
+
return "main";
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "master";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ── initLocalSession ──────────────────────────────────────────────────
|
|
34
|
+
export function initLocalSession(opts) {
|
|
35
|
+
const { url, token, session } = opts;
|
|
36
|
+
const dir = resolve(opts.dir);
|
|
37
|
+
const aUrl = authedUrl(url, token);
|
|
38
|
+
// Clone or update
|
|
39
|
+
if (!existsSync(dir)) {
|
|
40
|
+
execSync(`git clone --depth 1 --no-single-branch ${aUrl} ${dir}`, { stdio: "pipe" });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
git(`remote set-url origin ${aUrl}`, dir);
|
|
44
|
+
git("fetch origin", dir);
|
|
45
|
+
// Reset local default branch to latest remote
|
|
46
|
+
const defaultBranch = getDefaultBranch(dir);
|
|
47
|
+
git(`checkout ${defaultBranch}`, dir);
|
|
48
|
+
git(`reset --hard origin/${defaultBranch}`, dir);
|
|
49
|
+
}
|
|
50
|
+
// Determine branch
|
|
51
|
+
let branch;
|
|
52
|
+
let sessionId;
|
|
53
|
+
if (session) {
|
|
54
|
+
// Resume existing session
|
|
55
|
+
branch = session;
|
|
56
|
+
sessionId = branch.replace(/^gitclaw\/session-/, "") || branch;
|
|
57
|
+
// Try local checkout first, fall back to remote tracking
|
|
58
|
+
try {
|
|
59
|
+
git(`checkout ${branch}`, dir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
git(`checkout -b ${branch} origin/${branch}`, dir);
|
|
63
|
+
}
|
|
64
|
+
// Pull latest for existing session branch
|
|
65
|
+
try {
|
|
66
|
+
git(`pull origin ${branch}`, dir);
|
|
67
|
+
}
|
|
68
|
+
catch { /* branch may not exist on remote yet */ }
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// New session — branch off latest default branch
|
|
72
|
+
sessionId = randomBytes(4).toString("hex"); // 8-char hex
|
|
73
|
+
branch = `gitclaw/session-${sessionId}`;
|
|
74
|
+
git(`checkout -b ${branch}`, dir);
|
|
75
|
+
}
|
|
76
|
+
// Scaffold agent.yaml + memory if missing (on session branch only)
|
|
77
|
+
const agentYamlPath = `${dir}/agent.yaml`;
|
|
78
|
+
if (!existsSync(agentYamlPath)) {
|
|
79
|
+
const name = url.split("/").pop()?.replace(/\.git$/, "") || "agent";
|
|
80
|
+
writeFileSync(agentYamlPath, [
|
|
81
|
+
'spec_version: "0.1.0"',
|
|
82
|
+
`name: ${name}`,
|
|
83
|
+
"version: 0.1.0",
|
|
84
|
+
`description: Gitclaw agent for ${name}`,
|
|
85
|
+
"model:",
|
|
86
|
+
' preferred: "openai:gpt-4o-mini"',
|
|
87
|
+
" fallback: []",
|
|
88
|
+
"tools: [cli, read, write, memory]",
|
|
89
|
+
"runtime:",
|
|
90
|
+
" max_turns: 50",
|
|
91
|
+
"",
|
|
92
|
+
].join("\n"), "utf-8");
|
|
93
|
+
}
|
|
94
|
+
const memoryFile = `${dir}/memory/MEMORY.md`;
|
|
95
|
+
if (!existsSync(memoryFile)) {
|
|
96
|
+
mkdirSync(`${dir}/memory`, { recursive: true });
|
|
97
|
+
writeFileSync(memoryFile, "# Memory\n", "utf-8");
|
|
98
|
+
}
|
|
99
|
+
// Build session object
|
|
100
|
+
const localSession = {
|
|
101
|
+
dir,
|
|
102
|
+
branch,
|
|
103
|
+
sessionId,
|
|
104
|
+
commitChanges(msg) {
|
|
105
|
+
git("add -A", dir);
|
|
106
|
+
try {
|
|
107
|
+
git("diff --cached --quiet", dir);
|
|
108
|
+
// Nothing staged — skip
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// There are staged changes
|
|
112
|
+
const commitMsg = msg || `gitclaw: auto-commit (${branch})`;
|
|
113
|
+
git(`commit -m "${commitMsg}"`, dir);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
push() {
|
|
117
|
+
git(`push origin ${branch}`, dir);
|
|
118
|
+
},
|
|
119
|
+
finalize() {
|
|
120
|
+
localSession.commitChanges();
|
|
121
|
+
localSession.push();
|
|
122
|
+
// Strip PAT from remote URL
|
|
123
|
+
git(`remote set-url origin ${cleanUrl(url)}`, dir);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
return localSession;
|
|
127
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SkillMetadata {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
directory: string;
|
|
5
|
+
filePath: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsedSkill extends SkillMetadata {
|
|
8
|
+
instructions: string;
|
|
9
|
+
hasScripts: boolean;
|
|
10
|
+
hasReferences: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function discoverSkills(agentDir: string): Promise<SkillMetadata[]>;
|
|
13
|
+
export declare function loadSkill(meta: SkillMetadata): Promise<ParsedSkill>;
|
|
14
|
+
export declare function formatSkillsForPrompt(skills: SkillMetadata[]): string;
|
|
15
|
+
export declare function expandSkillCommand(input: string, skills: SkillMetadata[]): Promise<{
|
|
16
|
+
expanded: string;
|
|
17
|
+
skillName: string;
|
|
18
|
+
} | null>;
|