Skip to content

Commit 139c62d

Browse files
v0.7.5
# v0.7.5 — Network Proxy, Webhooks & Community Fixes --- ## Features - **Network proxy support** — configure HTTP/HTTPS proxies with bypass rules directly from App Settings. The proxy engine routes traffic through `undici` ProxyAgent instances, respects `NO_PROXY` rules, and configures both Node and Electron browser sessions. (46049d28, 0ed01c48, 81d1b1c3, 0da3f265) - **Webhook actions for automations** — automations can now fire HTTP webhooks with configurable auth, form payloads, response capture, replay, and persistent retry with exponential backoff. (993c75ce, 588aab6c) - **Gemini 3.1 Flash Lite** — added to Google AI Studio preferred defaults. Thanks to [@naishyadav](https://github.com/naishyadav) for the suggestion in [#357](#357). (e36e9a22) - **Dismiss working directory history items** — hover-visible X button on each recent directory entry to remove it from history. Thanks to [@jjjrmy](https://github.com/jjjrmy) ([#346](#346)) and [@jonzhan](https://github.com/jonzhan) ([#391](#391)) for requesting this. (3bd55d6d) ## Improvements - **History truncation** — consolidated all history field truncation to a single `HISTORY_FIELD_MAX_LENGTH` constant instead of scattered hardcoded limits (8689a1d4) - **Craft source docs** — added Collections section to guide with emphasis on the nested title + properties item format (0837afa0) ## Bug Fixes - **MiniMax CN authentication** — removed incorrect `minimax-cn → minimax` alias, added lightweight direct HTTP test for Pi providers, and stripped MiniMax-prefix for CN API compatibility. Thanks to [@Kathie-yu](https://github.com/Kathie-yu) ([#396](#396)) and [@RimuruW](https://github.com/RimuruW) ([#386](#386)) for reporting. Fixes [#396](#396). (612c0e7e) - **Inline code in messages** — text between inline badges (sources, skills, files) now renders through the Markdown component, restoring inline code, bold, italic, and links. Thanks to [@linusrogge](https://github.com/linusrogge) for reporting [#378](#378). Fixes [#378](#378). (e7f88a38) - **Zod/JSON Schema passthrough** — default Zod object schemas to `.passthrough()` to match JSON Schema semantics where `additionalProperties` defaults to `true`. Also preserved `additionalProperties` in MCP proxy tool schema round-trip. Fixes tools with loosely-typed schemas silently losing fields. (cf4b6ac1, 42c173cf) - **Self-signed TLS certificates** — accept self-signed certificates for the configured remote server origin, fixing `ERR_CERT_AUTHORITY_INVALID` on `wss://` connections (7148ebec) - **File attachment in thin client mode** — paperclip button now uses browser-native `FileReader` API instead of server-side `fs.readFile()`, fixing silent failures when client and server filesystems differ (680cd197) - **URL linkification** — strip trailing markdown characters (`**`, etc.) from linkified URLs that were producing broken links (24385e78, 65b8f350) - **Automation action badges** — sidebar now shows correct "Prompt" / "Webhook" badges based on actual action types instead of hardcoding "Prompt" (dc422573) - **Packaged server path fallback** — added `dist/resources` fallback for builds where `extraResources` output layout differs (edc5f61f) - **`$CRAFT_EVENT_DATA` missing labels** — all automation event payloads now include the session's current `labels` array, so webhooks and scripts can access label data. Fixes [#406](#406). (0f98f090) - **Multi-select non-adjacent sessions** — Cmd-click now always toggles selection (standard OS behavior); opening in a new panel moves to Cmd-Shift-click. Fixes [#404](#404). (d4d7aff1) - **@ mention autocomplete with spaces** — spaces are now allowed in file mention queries (e.g. `@app availability.md`). The menu auto-closes Slack-style when a space produces no matches. Thanks to [@alexzadeh](https://github.com/alexzadeh) for reporting [#398](#398). Fixes [#398](#398). (1d063177) ---
1 parent dc61e3e commit 139c62d

File tree

86 files changed

+3772
-259
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+3772
-259
lines changed

apps/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@craft-agent/cli",
3-
"version": "0.7.4",
3+
"version": "0.7.5",
44
"description": "Terminal client for Craft Agent server",
55
"type": "module",
66
"main": "src/index.ts",

apps/cli/src/commands.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,11 @@ describe('parseArgs', () => {
234234
import { getValidateSteps } from './index.ts'
235235

236236
describe('getValidateSteps', () => {
237-
it('returns 21 steps', () => {
237+
it('returns a non-empty array of steps', () => {
238238
const steps = getValidateSteps()
239-
expect(steps.length).toBe(27)
239+
expect(steps.length).toBeGreaterThan(0)
240+
// Sanity: at least the known lifecycle groups exist
241+
expect(steps.length).toBeGreaterThanOrEqual(20)
240242
})
241243

242244
it('first step is handshake', () => {
@@ -249,6 +251,11 @@ describe('getValidateSteps', () => {
249251
expect(steps[steps.length - 1].name).toBe('Disconnect')
250252
})
251253

254+
it('has no duplicate step names', () => {
255+
const names = getValidateSteps().map((s) => s.name)
256+
expect(new Set(names).size).toBe(names.length)
257+
})
258+
252259
it('includes session lifecycle steps (create, read, delete)', () => {
253260
const steps = getValidateSteps()
254261
const names = steps.map((s) => s.name)
@@ -285,6 +292,29 @@ describe('getValidateSteps', () => {
285292
expect(names).toContain('skills:delete')
286293
})
287294

295+
it('includes automation lifecycle steps', () => {
296+
const names = getValidateSteps().map((s) => s.name)
297+
expect(names).toContain('automation:create')
298+
expect(names).toContain('automation:trigger (status change)')
299+
expect(names).toContain('automation:verify session')
300+
expect(names).toContain('automation:verify labels')
301+
expect(names).toContain('automations:getLastExecuted')
302+
expect(names).toContain('automation:cleanup')
303+
})
304+
305+
it('includes session branching steps', () => {
306+
const names = getValidateSteps().map((s) => s.name)
307+
expect(names).toContain('sessions:branch')
308+
expect(names).toContain('sessions:branch verify')
309+
expect(names).toContain('sessions:branch send')
310+
})
311+
312+
it('includes webhook validation steps', () => {
313+
const names = getValidateSteps().map((s) => s.name)
314+
expect(names).toContain('webhook:test (RPC)')
315+
expect(names).toContain('webhook:verify failure')
316+
})
317+
288318
it('creates session with allow-all permission mode', () => {
289319
const steps = getValidateSteps()
290320
const createStep = steps.find((s) => s.name === 'sessions:create')
@@ -302,4 +332,19 @@ describe('getValidateSteps', () => {
302332
expect(sourceDelete).toBeGreaterThan(skillDelete)
303333
expect(sessionDelete).toBeGreaterThan(sourceDelete)
304334
})
335+
336+
it('branching steps come after send message + tool use', () => {
337+
const names = getValidateSteps().map((s) => s.name)
338+
const toolUse = names.indexOf('send message + tool use')
339+
const branch = names.indexOf('sessions:branch')
340+
expect(branch).toBeGreaterThan(toolUse)
341+
})
342+
343+
it('automation cleanup comes before sources:delete', () => {
344+
const names = getValidateSteps().map((s) => s.name)
345+
const cleanup = names.indexOf('automation:cleanup')
346+
const srcDelete = names.indexOf('sources:delete')
347+
expect(cleanup).toBeGreaterThan(-1)
348+
expect(cleanup).toBeLessThan(srcDelete)
349+
})
305350
})

apps/cli/src/index.ts

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,7 @@ export interface ValidateContext {
777777
automationsJsonBackup?: string | null
778778
/** Backup of existing automations-history.jsonl before overwrite (undefined = didn't exist) */
779779
automationsHistoryBackup?: string | null
780+
branchedSessionId?: string
780781
onEvent?: (ev: { type: string; [key: string]: unknown }) => void
781782
}
782783

@@ -1054,6 +1055,49 @@ export function getValidateSteps(): ValidateStep[] {
10541055
'Use the Bash tool to run: echo TOOL_VALIDATION_OK', 90_000, true, undefined, ctx.onEvent)
10551056
},
10561057
},
1058+
// ----- Session branching -----
1059+
{
1060+
name: 'sessions:branch',
1061+
fn: async (client, ctx) => {
1062+
if (!ctx.createdSessionId || !ctx.workspaceId) return 'skipped (no session)'
1063+
const r = (await client.invoke('sessions:getMessages', ctx.createdSessionId)) as ValidateMessagesResponse
1064+
const messages = r?.messages ?? r?.conversation ?? []
1065+
const firstAssistant = messages.find((m) => m.role === 'assistant') as any
1066+
if (!firstAssistant?.id) throw new Error('No assistant message found to branch from')
1067+
const branch = (await client.invoke('sessions:create', ctx.workspaceId, {
1068+
name: `__cli-validate-branch-${Date.now()}`,
1069+
permissionMode: 'allow-all',
1070+
branchFromSessionId: ctx.createdSessionId,
1071+
branchFromMessageId: firstAssistant.id,
1072+
})) as any
1073+
ctx.branchedSessionId = branch?.id
1074+
return `branched at message ${firstAssistant.id} → session ${branch?.id}`
1075+
},
1076+
},
1077+
{
1078+
name: 'sessions:branch verify',
1079+
fn: async (client, ctx) => {
1080+
if (!ctx.branchedSessionId) return 'skipped (no branch)'
1081+
const r = (await client.invoke('sessions:getMessages', ctx.branchedSessionId)) as ValidateMessagesResponse
1082+
const messages = r?.messages ?? r?.conversation ?? []
1083+
const hasAssistant = messages.some((m) => m.role === 'assistant')
1084+
if (!hasAssistant) throw new Error('Branch missing assistant message')
1085+
const origR = (await client.invoke('sessions:getMessages', ctx.createdSessionId!)) as ValidateMessagesResponse
1086+
const origMessages = origR?.messages ?? origR?.conversation ?? []
1087+
if (messages.length >= origMessages.length) {
1088+
throw new Error(`Branch has ${messages.length} messages, expected fewer than original (${origMessages.length})`)
1089+
}
1090+
return `branch has ${messages.length} messages (original has ${origMessages.length})`
1091+
},
1092+
},
1093+
{
1094+
name: 'sessions:branch send',
1095+
fn: async (client, ctx) => {
1096+
if (!ctx.branchedSessionId) return 'skipped (no branch)'
1097+
return await waitForSendEvents(client, ctx.branchedSessionId,
1098+
'Reply with exactly: BRANCH_OK', 60_000, false, undefined, ctx.onEvent)
1099+
},
1100+
},
10571101
// ----- Source lifecycle -----
10581102
{
10591103
name: 'sources:create',
@@ -1142,30 +1186,48 @@ SKILLEOF`, 90_000, true, undefined, ctx.onEvent)
11421186
name: 'automation:create',
11431187
fn: async (client, ctx) => {
11441188
if (!ctx.createdSessionId || !ctx.workspaceRootPath) return 'skipped (no session or workspace)'
1145-
ctx.automationName = `CLI Validate Automation ${Date.now()}`
11461189
const configPath = `${ctx.workspaceRootPath}/automations.json`
11471190
const historyPath = `${ctx.workspaceRootPath}/automations-history.jsonl`
1148-
// Backup existing files before overwriting (protects real workspace data)
1149-
const { readFile } = await import('fs/promises')
1150-
ctx.automationsJsonBackup = await readFile(configPath, 'utf-8').catch(() => null)
1191+
const { readFile, writeFile } = await import('fs/promises')
1192+
1193+
// Check if config already exists (CI case — .github/agents/automations.json committed)
1194+
const existingConfig = await readFile(configPath, 'utf-8').catch(() => null)
1195+
if (existingConfig) {
1196+
try {
1197+
const parsed = JSON.parse(existingConfig)
1198+
const entries = parsed?.automations?.SessionStatusChange
1199+
if (Array.isArray(entries) && entries.length > 0) {
1200+
ctx.automationName = entries[0].name
1201+
return `config already loaded (${ctx.automationName})`
1202+
}
1203+
} catch { /* parse failed, overwrite below */ }
1204+
}
1205+
1206+
// Non-CI: backup + write directly (no LLM call needed)
1207+
ctx.automationsJsonBackup = existingConfig
11511208
ctx.automationsHistoryBackup = await readFile(historyPath, 'utf-8').catch(() => null)
1209+
ctx.automationName = `CLI Validate Automation ${Date.now()}`
11521210
const config = JSON.stringify({
11531211
version: 2,
11541212
automations: {
11551213
SessionStatusChange: [{
11561214
name: ctx.automationName,
11571215
matcher: 'in-progress',
11581216
labels: ['cli-validate-label'],
1159-
actions: [{ type: 'prompt', prompt: 'Reply with exactly: AUTOMATION_TRIGGERED' }],
1217+
actions: [
1218+
{ type: 'prompt', prompt: 'Reply with exactly: AUTOMATION_TRIGGERED' },
1219+
{ type: 'webhook', url: 'http://127.0.0.1:19999/validate-webhook',
1220+
method: 'POST', bodyFormat: 'json',
1221+
body: { event: '$CRAFT_EVENT', session: '$CRAFT_SESSION_ID' } },
1222+
],
11601223
}],
11611224
},
11621225
}, null, 2)
1163-
return await waitForSendEvents(client, ctx.createdSessionId,
1164-
`Use the Bash tool to run this exact command:
1165-
cat > "${configPath}" << 'AUTOMATIONEOF'
1166-
${config}
1167-
AUTOMATIONEOF`, 90_000, true, undefined, ctx.onEvent)
1168-
.then((r) => { ctx.createdAutomation = true; return r })
1226+
await writeFile(configPath, config)
1227+
ctx.createdAutomation = true
1228+
// ConfigWatcher auto-detects automations.json changes (debounced)
1229+
await new Promise((r) => setTimeout(r, 2000))
1230+
return `wrote config (${ctx.automationName})`
11691231
},
11701232
},
11711233
{
@@ -1263,13 +1325,62 @@ AUTOMATIONEOF`, 90_000, true, undefined, ctx.onEvent)
12631325
return `${entries.length} automation(s), latest ran ${Math.round((Date.now() - recent[1]) / 1000)}s ago`
12641326
},
12651327
},
1328+
// ----- Webhook validation -----
1329+
{
1330+
name: 'webhook:test (RPC)',
1331+
fn: async (client, ctx) => {
1332+
if (!ctx.workspaceId) return 'skipped (no workspace)'
1333+
const r = (await client.invoke('automations:test', {
1334+
workspaceId: ctx.workspaceId,
1335+
actions: [{
1336+
type: 'webhook',
1337+
url: 'http://127.0.0.1:19999/validate-test',
1338+
method: 'GET',
1339+
}],
1340+
})) as any
1341+
const result = r?.actions?.[0]
1342+
if (result?.success) throw new Error('Expected webhook to fail (nothing listening)')
1343+
if (!result?.error && result?.statusCode !== 0) throw new Error('Expected error or statusCode 0 in result')
1344+
return `correctly failed: ${(result.error ?? `statusCode=${result.statusCode}`).slice(0, 80)}`
1345+
},
1346+
},
1347+
{
1348+
name: 'webhook:verify failure',
1349+
fn: async (client, ctx) => {
1350+
if (!ctx.workspaceRootPath) return 'skipped (no workspace root)'
1351+
const { readFile } = await import('fs/promises')
1352+
const historyPath = `${ctx.workspaceRootPath}/automations-history.jsonl`
1353+
const content = await readFile(historyPath, 'utf-8').catch(() => '')
1354+
const lines = content.trim().split('\n').filter(Boolean)
1355+
const entries = lines.map((l) => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean)
1356+
const webhookEntries = entries.filter((e: any) => e.webhook)
1357+
if (webhookEntries.length === 0) throw new Error('No webhook history entries found')
1358+
// Find a recent failed webhook entry (within last 2 minutes)
1359+
const recentThreshold = Date.now() - 120_000
1360+
const recentFailed = webhookEntries.find((e: any) =>
1361+
!e.ok && e.ts > recentThreshold && e.webhook?.method === 'POST'
1362+
)
1363+
if (!recentFailed) throw new Error('No recent failed webhook entry found in history')
1364+
return `webhook failure recorded: method=${recentFailed.webhook.method}, url=${recentFailed.webhook.url?.slice(0, 50)}`
1365+
},
1366+
},
12661367
{
12671368
name: 'automation:cleanup',
12681369
fn: async (client, ctx) => {
12691370
const cleaned = await cleanupAutomationArtifacts(client, ctx)
12701371
return cleaned.length > 0 ? `cleaned: ${cleaned.join(', ')}` : 'nothing to clean'
12711372
},
12721373
},
1374+
{
1375+
name: 'sessions:branch delete',
1376+
fn: async (client, ctx) => {
1377+
if (!ctx.branchedSessionId) return 'skipped (no branch)'
1378+
await client.invoke('sessions:delete', ctx.branchedSessionId)
1379+
const id = ctx.branchedSessionId
1380+
ctx.branchedSessionId = undefined
1381+
return `deleted branch session: ${id}`
1382+
},
1383+
},
12731384
{
12741385
name: 'sources:delete',
12751386
fn: async (client, ctx) => {
@@ -1413,6 +1524,15 @@ export async function runValidation(client: CliRpcClient, jsonMode: boolean, noS
14131524
}
14141525
}
14151526

1527+
// Cleanup: branched session
1528+
if (ctx.branchedSessionId && client.isConnected) {
1529+
try {
1530+
await client.invoke('sessions:delete', ctx.branchedSessionId)
1531+
} catch {
1532+
// best effort
1533+
}
1534+
}
1535+
14161536
// Cleanup: if a session was created but delete step hasn't run or failed
14171537
if (ctx.createdSessionId && client.isConnected) {
14181538
try {

apps/electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@craft-agent/electron",
3-
"version": "0.7.4",
3+
"version": "0.7.5",
44
"description": "Electron desktop app for Craft Agents",
55
"main": "dist/main.cjs",
66
"private": true,

0 commit comments

Comments
 (0)