@voidlabs.gg/voidshift-agents 0.6.0 → 0.8.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/engine.js CHANGED
@@ -2,15 +2,29 @@ import { spawn } from 'node:child_process';
2
2
  import { createWriteStream, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { usageText } from "./state.js";
5
6
  // Tools a write step may use under `claude -p`. Anything else is denied,
6
7
  // since nobody is there to approve it.
7
8
  const CLAUDE_WRITE = ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'];
8
9
  const CLAUDE_READ = ['Read', 'Glob', 'Grep', 'Bash(git diff:*)', 'Bash(git log:*)', 'Bash(git show:*)', 'Bash(npm test:*)', 'Bash(npm run:*)', 'Bash(pnpm:*)', 'Bash(yarn:*)', 'Bash(npx:*)', 'Bash(node:*)', 'Bash(ls:*)', 'Bash(rg:*)'];
9
10
  export function engineArgs(o, outFile) {
11
+ // Nobody is there to approve a prompt, and an allowlist that misses a tool
12
+ // breaks the run, so on Linux every step runs with permissions skipped.
13
+ const skip = (o.platform ?? process.platform) === 'linux';
14
+ if (o.engine === 'claude' && skip) {
15
+ const args = ['-p', '--output-format', 'json', '--dangerously-skip-permissions'];
16
+ if (o.vault)
17
+ args.push('--add-dir', o.vault);
18
+ if (o.model)
19
+ args.push('--model', o.model);
20
+ if (o.settingsFile)
21
+ args.push('--settings', o.settingsFile);
22
+ return { cmd: 'claude', args };
23
+ }
10
24
  if (o.engine === 'claude') {
11
25
  // Edit rules cover every file-writing tool; a Write(path) rule is ignored.
12
26
  const vaultTools = o.vault ? [`Edit(/${o.vault}/**)`] : [];
13
- const args = ['-p', '--output-format', 'text', '--allowedTools', [...(o.write ? CLAUDE_WRITE : CLAUDE_READ), ...vaultTools].join(',')];
27
+ const args = ['-p', '--output-format', 'json', '--allowedTools', [...(o.write ? CLAUDE_WRITE : CLAUDE_READ), ...vaultTools].join(',')];
14
28
  if (o.vault)
15
29
  args.push('--add-dir', o.vault);
16
30
  if (o.write)
@@ -21,6 +35,13 @@ export function engineArgs(o, outFile) {
21
35
  args.push('--settings', o.settingsFile);
22
36
  return { cmd: 'claude', args };
23
37
  }
38
+ if (skip) {
39
+ const args = ['exec', '-C', o.cwd, '--dangerously-bypass-approvals-and-sandbox', '-o', outFile];
40
+ if (o.model)
41
+ args.push('-m', o.model);
42
+ args.push('-');
43
+ return { cmd: 'codex', args };
44
+ }
24
45
  const args = ['exec', '-C', o.cwd, '-s', o.write ? 'workspace-write' : 'read-only', '-o', outFile];
25
46
  // A read-only sandbox cannot write anywhere, so only write steps get the vault.
26
47
  if (o.vault && o.write)
@@ -49,8 +70,52 @@ export async function stopAgents() {
49
70
  if (c.exitCode === null && c.signalCode === null)
50
71
  c.kill('SIGKILL');
51
72
  }
73
+ // Why a failed agent process failed: the last lines it wrote to stderr.
74
+ export function errorTail(stderr) {
75
+ const lines = stderr.split('\n').map((l) => l.trim()).filter(Boolean);
76
+ let out = '';
77
+ for (const line of lines.reverse()) {
78
+ const next = out ? `${line} ${out}` : line;
79
+ if (next.length > 300)
80
+ break;
81
+ out = next;
82
+ }
83
+ return out || lines[0]?.slice(-300) || '';
84
+ }
85
+ // Claude's JSON answer: its final text, and the tokens and cost it spent.
86
+ // Null when the output is not that JSON, so it is taken as plain text.
87
+ export function parseClaudeJson(stdout) {
88
+ let j;
89
+ try {
90
+ j = JSON.parse(stdout);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ if (!j || typeof j !== 'object' || typeof j.result !== 'string')
96
+ return null;
97
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
98
+ const usage = {};
99
+ const models = j.modelUsage && typeof j.modelUsage === 'object' ? Object.entries(j.modelUsage) : [];
100
+ if (models.length) {
101
+ // Every model the step used, side models included.
102
+ usage.input = models.reduce((t, [, m]) => t + num(m?.inputTokens), 0);
103
+ usage.output = models.reduce((t, [, m]) => t + num(m?.outputTokens), 0);
104
+ usage.cached = models.reduce((t, [, m]) => t + num(m?.cacheReadInputTokens) + num(m?.cacheCreationInputTokens), 0);
105
+ usage.model = models.reduce((best, cur) => (num(cur[1]?.costUSD) > num(best[1]?.costUSD) ? cur : best))[0];
106
+ }
107
+ else if (j.usage && typeof j.usage === 'object') {
108
+ usage.input = num(j.usage.input_tokens);
109
+ usage.output = num(j.usage.output_tokens);
110
+ usage.cached = num(j.usage.cache_read_input_tokens) + num(j.usage.cache_creation_input_tokens);
111
+ }
112
+ if (typeof j.total_cost_usd === 'number')
113
+ usage.costUsd = j.total_cost_usd;
114
+ return { text: j.result, isError: j.is_error === true, usage };
115
+ }
52
116
  // Runs one agent step with the prompt on stdin. Resolves with the agent's
53
- // final text; everything it prints also goes to the log file.
117
+ // final text; what it prints also goes to the log file, Claude's answer as
118
+ // its text and a usage line. A process that fails also says why in `error`.
54
119
  export function runAgent(o) {
55
120
  const dir = mkdtempSync(join(tmpdir(), 'voidshift-run-'));
56
121
  const outFile = join(dir, 'last.txt');
@@ -63,24 +128,52 @@ export function runAgent(o) {
63
128
  const env = { ...process.env, VOIDSHIFT_RECALL: 'off' };
64
129
  const child = spawn(cmd, args, { cwd: o.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
65
130
  track(child);
131
+ // Claude's JSON comes at exit, so it is held and logged as text then.
66
132
  let stdout = '';
67
133
  child.stdout.on('data', (d) => {
68
134
  stdout += d;
135
+ if (o.engine !== 'claude')
136
+ log.write(d);
137
+ });
138
+ // Only the tail is kept; the log has all of it.
139
+ let stderr = '';
140
+ child.stderr.on('data', (d) => {
141
+ stderr = (stderr + d).split('\n').slice(-20).join('\n');
69
142
  log.write(d);
70
143
  });
71
- child.stderr.on('data', (d) => log.write(d));
72
- const timer = setTimeout(() => child.kill('SIGTERM'), o.timeoutMs ?? 45 * 60_000);
144
+ const timeoutMs = o.timeoutMs ?? 45 * 60_000;
145
+ let timedOut = false;
146
+ const timer = setTimeout(() => {
147
+ timedOut = true;
148
+ child.kill('SIGTERM');
149
+ }, timeoutMs);
73
150
  child.on('error', (err) => {
74
151
  clearTimeout(timer);
75
152
  children.delete(child);
76
153
  log.end(`\n${err.message}\n`);
77
- resolve({ ok: false, output: err.message });
154
+ resolve({ ok: false, output: err.message, error: err.message });
78
155
  });
79
156
  child.on('close', (code) => {
80
157
  clearTimeout(timer);
81
158
  children.delete(child);
82
- log.end(`\nexit ${code}\n`);
83
159
  let output = stdout;
160
+ let usage;
161
+ let failure = '';
162
+ if (o.engine === 'claude') {
163
+ const parsed = parseClaudeJson(stdout);
164
+ if (parsed) {
165
+ output = parsed.text;
166
+ if (Object.keys(parsed.usage).length)
167
+ usage = parsed.usage;
168
+ if (parsed.isError)
169
+ failure = parsed.text;
170
+ log.write(`${output}\n\nusage: ${[usageText(usage), usage?.model].filter(Boolean).join(', ') || 'none reported'}\n`);
171
+ }
172
+ else
173
+ log.write(stdout);
174
+ }
175
+ else if (o.model)
176
+ usage = { model: o.model };
84
177
  if (o.engine === 'codex') {
85
178
  try {
86
179
  output = readFileSync(outFile, 'utf8');
@@ -90,8 +183,52 @@ export function runAgent(o) {
90
183
  }
91
184
  }
92
185
  rmSync(dir, { recursive: true, force: true });
93
- resolve({ ok: code === 0, output });
186
+ const minutes = Math.round(timeoutMs / 60_000);
187
+ const error = code === 0 ? undefined : timedOut ? `timed out after ${minutes} minute${minutes === 1 ? '' : 's'}` : errorTail(stderr) || errorTail(failure) || `exit ${code}`;
188
+ // Settled once the log is written, so the step's log is whole when read.
189
+ log.end(`\nexit ${code}\n`, () => resolve({ ok: code === 0, output, ...(error ? { error } : {}), ...(usage ? { usage } : {}) }));
94
190
  });
95
191
  child.stdin.end(o.prompt);
96
192
  });
97
193
  }
194
+ // Runs one command step in a shell in place of an agent. Resolves with what it
195
+ // printed on stdout, which is also logged along with stderr; a command that
196
+ // fails also says why in `error`.
197
+ export function runCommand(o) {
198
+ const log = createWriteStream(o.logFile, { flags: 'a' });
199
+ log.write(`$ ${o.command}\n\n`);
200
+ return new Promise((resolve) => {
201
+ const child = spawn('sh', ['-c', o.command], { cwd: o.cwd, env: { ...process.env, ...o.env }, stdio: ['ignore', 'pipe', 'pipe'] });
202
+ track(child);
203
+ let stdout = '';
204
+ child.stdout.on('data', (d) => {
205
+ stdout += d;
206
+ log.write(d);
207
+ });
208
+ // Only the tail is kept; the log has all of it.
209
+ let stderr = '';
210
+ child.stderr.on('data', (d) => {
211
+ stderr = (stderr + d).split('\n').slice(-20).join('\n');
212
+ log.write(d);
213
+ });
214
+ const timeoutMs = o.timeoutMs ?? 45 * 60_000;
215
+ let timedOut = false;
216
+ const timer = setTimeout(() => {
217
+ timedOut = true;
218
+ child.kill('SIGTERM');
219
+ }, timeoutMs);
220
+ child.on('error', (err) => {
221
+ clearTimeout(timer);
222
+ children.delete(child);
223
+ log.end(`\n${err.message}\n`);
224
+ resolve({ ok: false, output: err.message, error: err.message });
225
+ });
226
+ child.on('close', (code) => {
227
+ clearTimeout(timer);
228
+ children.delete(child);
229
+ const minutes = Math.round(timeoutMs / 60_000);
230
+ const error = code === 0 ? undefined : timedOut ? `timed out after ${minutes} minute${minutes === 1 ? '' : 's'}` : errorTail(stderr) || `exit ${code}`;
231
+ log.end(`\nexit ${code}\n`, () => resolve({ ok: code === 0, output: stdout, ...(error ? { error } : {}) }));
232
+ });
233
+ });
234
+ }
package/dist/git.d.ts CHANGED
@@ -34,3 +34,4 @@ export interface CommitPolicy {
34
34
  }
35
35
  export declare function applyPolicy(message: string, policy: CommitPolicy, signer: string | null, cwd: string): string;
36
36
  export declare function enforceCommitPolicy(worktree: string, from: string, policy: CommitPolicy): number;
37
+ export declare function removeWorktree(repo: string, worktree: string, branch: string): void;
package/dist/git.js CHANGED
@@ -194,3 +194,8 @@ export function enforceCommitPolicy(worktree, from, policy) {
194
194
  must(sh('git', ['reset', '-q', '--soft', head], worktree), 'git reset');
195
195
  return changed;
196
196
  }
197
+ // Removes a worktree and its branch. Errors are ignored: either may be gone already.
198
+ export function removeWorktree(repo, worktree, branch) {
199
+ sh('git', ['worktree', 'remove', '--force', worktree], repo);
200
+ sh('git', ['branch', '-D', branch], repo);
201
+ }
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { controlChecks, controlCommands, controlUsage } from "./control.js";
9
9
  import { keyStore, looksLikeLinearKey, readLinearKey, readSecret, storeLinearKey, storeSecret } from "./secrets.js";
10
10
  import { TRACKERS } from "./tracker.js";
11
11
  import { createVoidTrack } from "./voidtrack.js";
12
+ import { addWebhook, dropWebhook, looksLikeWebhook, moveWebhook, removeWebhook, testWebhook, webhookFor } from "./notify.js";
12
13
  import { parseCron } from "./cron.js";
13
14
  import { trackersInUse } from "./control.js";
14
15
  import { runnerService } from "./service.js";
@@ -379,6 +380,7 @@ const USAGE = [
379
380
  ['link DIR [--claude | --codex]', 'Link the agents into a config folder'],
380
381
  ['unlink DIR', 'Remove the agents from a config folder'],
381
382
  ['key [--voidtrack]', 'Store or replace the Linear or VoidTrack API key (reads stdin without a terminal)'],
383
+ ['discord [--workflow W] [--test] [--rm]', 'Store, test or remove the Discord webhook for run pings, global or for one workflow; it is stored only after a test message goes through (reads stdin without a terminal)'],
382
384
  ['profile [init]', `Your profile: rules (${RULES_FILE}) and Claude settings (${CLAUDE_SETTINGS}) every agent run carries; init creates it`],
383
385
  ['validate [DIR]', `Check an agent folder (DIR/${AGENT_FILE} and its prompts), or the config and every folder under agents/defs`],
384
386
  ['help [COMMAND]', 'Show this list, or one command'],
@@ -662,8 +664,14 @@ const workflowSubs = {
662
664
  if (!teams)
663
665
  return 1;
664
666
  const r = await promptWorkflow(ctx.ui, config, teams, { name, workflow: current }, true);
665
- if (r.name !== name)
667
+ if (r.name !== name) {
668
+ const refused = moveWebhook(ctx.configDir, name, r.name);
669
+ if (refused) {
670
+ ctx.log(refused);
671
+ return 1;
672
+ }
666
673
  config.workflows = renameKey(config.workflows, name, r.name);
674
+ }
667
675
  config.workflows[r.name] = r.workflow;
668
676
  return saveWorkflow(config, r.name, r.workflow, ctx);
669
677
  }
@@ -716,6 +724,11 @@ const workflowSubs = {
716
724
  ctx.log(`A workflow named ${next} already exists.`);
717
725
  return 1;
718
726
  }
727
+ const refused = moveWebhook(ctx.configDir, old, next);
728
+ if (refused) {
729
+ ctx.log(refused);
730
+ return 1;
731
+ }
719
732
  config.workflows = renameKey(config.workflows, old, next);
720
733
  save(ctx, config);
721
734
  ctx.log(`Workflow ${old} is now ${next}.`);
@@ -742,6 +755,7 @@ const workflowSubs = {
742
755
  }
743
756
  delete config.workflows[name];
744
757
  save(ctx, config);
758
+ dropWebhook(ctx.configDir, name);
745
759
  ctx.log(`Deleted workflow ${name}.`);
746
760
  return 0;
747
761
  },
@@ -1220,6 +1234,11 @@ const agents = {
1220
1234
  if (!(await setupKey(ui, ctx.configDir, 'voidtrack')))
1221
1235
  ctx.log('VoidTrack is not connected yet. Run vsf agents key --voidtrack.');
1222
1236
  }
1237
+ if (!webhookFor(ctx.configDir) && (await ui.confirm('Send run pings to Discord?', false))) {
1238
+ const url = await ui.secret('Paste the Discord webhook URL', (v) => (looksLikeWebhook(v.trim()) ? undefined : 'Not a Discord webhook URL'));
1239
+ if (!(await addWebhook(ctx.configDir, url.trim(), undefined, ui.info)))
1240
+ ctx.log('Discord is not connected. Run vsf agents discord to retry.');
1241
+ }
1223
1242
  if (Object.keys(config.workflows).length && (await ui.confirm('Start the background runner now? Every minute it checks for cards and schedules that start your workflows.'))) {
1224
1243
  const ok = runnerService().start(ctx.home, [...ctx.selfCommand, 'agents', 'tick'], join(agentsDir(ctx.configDir), 'runner.log'));
1225
1244
  ctx.log(ok ? 'Runner started.' : 'Could not start the runner. Try vsf agents start.');
@@ -1258,6 +1277,8 @@ const agents = {
1258
1277
  checks.push({ name, status: 'fail', detail: err.message, fix: `vsf agents key${kind === 'linear' ? '' : ` --${kind}`}` });
1259
1278
  }
1260
1279
  }
1280
+ if (webhookFor(ctx.configDir))
1281
+ checks.push({ name: 'discord', status: 'ok', detail: 'webhook set' });
1261
1282
  const workflows = Object.entries(saved.workflows);
1262
1283
  const broken = workflows.filter(([, c]) => !saved.projects[c.project] || !isRepo(saved.projects[c.project].path)).map(([n]) => n);
1263
1284
  checks.push(!workflows.length
@@ -1389,6 +1410,38 @@ const agents = {
1389
1410
  }
1390
1411
  return (await verifyAndStore(ctx.configDir, key, ctx.log, ctx.log, kind)) ? 0 : 1;
1391
1412
  },
1413
+ async discord(args, ctx) {
1414
+ const p = begin('discord', args, { workflow: { type: 'string' }, test: { type: 'boolean' }, rm: { type: 'boolean' } }, ctx);
1415
+ if (typeof p === 'number')
1416
+ return p;
1417
+ if (p.positionals.length) {
1418
+ ctx.log('Webhooks are not accepted on the command line. Pipe the URL in instead.');
1419
+ return 2;
1420
+ }
1421
+ if (p.values.test && p.values.rm) {
1422
+ ctx.log('Give --test or --rm, not both.');
1423
+ return 2;
1424
+ }
1425
+ let workflow;
1426
+ if (p.values.workflow) {
1427
+ const name = pickWorkflow(readAgentsConfig(ctx.configDir), p.values.workflow, ctx);
1428
+ if (!name)
1429
+ return 1;
1430
+ workflow = name;
1431
+ }
1432
+ if (p.values.test)
1433
+ return (await testWebhook(ctx.configDir, workflow, ctx.log)) ? 0 : 1;
1434
+ if (p.values.rm)
1435
+ return removeWebhook(ctx.configDir, workflow, ctx.log) ? 0 : 1;
1436
+ const url = interactive()
1437
+ ? await ctx.ui.secret('Paste the Discord webhook URL', (v) => (looksLikeWebhook(v.trim()) ? undefined : 'Not a Discord webhook URL'))
1438
+ : readFileSync(0, 'utf8');
1439
+ if (!url.trim()) {
1440
+ ctx.log('No webhook on stdin.');
1441
+ return 2;
1442
+ }
1443
+ return (await addWebhook(ctx.configDir, url.trim(), workflow, ctx.log)) ? 0 : 1;
1444
+ },
1392
1445
  async help(args, ctx) {
1393
1446
  const want = args.join(' ').trim();
1394
1447
  if (!want) {
package/dist/manage.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type AgentsConfig, type Step, type Trigger, type Workflow } from './config.ts';
2
- export declare function validateStep(step: Step, config: Pick<AgentsConfig, 'enabled'>, dir?: string): string[];
3
- export declare function validateWorkflow(config: Pick<AgentsConfig, 'enabled' | 'projects'>, workflow: Workflow, dir?: string): string[];
2
+ export declare function validateStep(step: Step, config: Pick<AgentsConfig, 'enabled'> & Partial<Pick<AgentsConfig, 'workflows'>>, dir?: string): string[];
3
+ export declare function validateWorkflow(config: Pick<AgentsConfig, 'enabled' | 'projects'> & Partial<Pick<AgentsConfig, 'workflows'>>, workflow: Workflow, dir?: string): string[];
4
4
  export declare function validateAgentFolder(dir: string, config: Pick<AgentsConfig, 'enabled' | 'projects' | 'workflows' | 'folders'>): string[];
5
5
  export declare const TRIGGER_FORMS = "TEAM:LABEL (Linear), voidtrack:TEAM:LABEL, or cron:SCHEDULE:TASK";
6
6
  export declare function parseTrigger(input: string): Trigger | null;
package/dist/manage.js CHANGED
@@ -29,13 +29,25 @@ export function validateStep(step, config, dir) {
29
29
  problems.push(`${name} prompt must be a file name.`);
30
30
  if (step.skill !== undefined && typeof step.skill !== 'string')
31
31
  problems.push(`${name} skill must be a name or a folder.`);
32
- const sources = [step.skill, step.prompt].filter((v) => v !== undefined).length;
32
+ if (step.run !== undefined && (typeof step.run !== 'string' || !step.run.trim()))
33
+ problems.push(`${name} run must be a command.`);
34
+ if (step.handoff !== undefined) {
35
+ if (typeof step.handoff !== 'string' || !step.handoff.trim())
36
+ problems.push(`${name} handoff must be a workflow name.`);
37
+ else if (config.workflows && !Object.hasOwn(config.workflows, step.handoff))
38
+ problems.push(`${name} hands cards to ${step.handoff}, which is not a workflow.`);
39
+ }
40
+ const sources = [step.skill, step.prompt, step.run].filter((v) => v !== undefined).length;
33
41
  if (step.output === 'publish') {
34
42
  if (sources)
35
- problems.push(`${name} only pushes the branch, so it takes no skill or prompt.`);
43
+ problems.push(`${name} only pushes the branch, so it takes no skill, prompt or run.`);
36
44
  }
37
45
  else if (sources !== 1)
38
- problems.push(`${name} needs exactly one of skill or prompt.`);
46
+ problems.push(`${name} needs exactly one of skill, prompt or run.`);
47
+ else if (step.run !== undefined) {
48
+ if (step.output !== 'none')
49
+ problems.push(`${name} runs a command, so its output must be none.`);
50
+ }
39
51
  else if (typeof (step.prompt ?? step.skill) === 'string') {
40
52
  const file = stepFile(step, dir);
41
53
  if (!file || !existsSync(file))
@@ -121,8 +133,8 @@ export function validateAgentFolder(dir, config) {
121
133
  catch (err) {
122
134
  return [`${AGENT_FILE}: ${err.message}`];
123
135
  }
124
- const problems = validateWorkflow(config, workflow, dir);
125
136
  const name = basename(dir);
137
+ const problems = validateWorkflow({ ...config, workflows: { ...config.workflows, [name]: workflow } }, workflow, dir);
126
138
  const from = config.folders?.[name];
127
139
  if (config.workflows[name] && (!from || resolve(from) !== resolve(dir)))
128
140
  problems.push(`config.json already has a workflow named ${name}, and it wins. Rename the folder.`);
@@ -228,6 +240,10 @@ export function validateConfig(value, saved) {
228
240
  problems.push('reviewState must be a non-empty string.');
229
241
  if (!isInt(value.timeoutMinutes, 1, 480))
230
242
  problems.push('timeoutMinutes must be a whole number from 1 to 480.');
243
+ if (value.maxRuns !== undefined && !isInt(value.maxRuns, 0, 32))
244
+ problems.push('maxRuns must be a whole number from 0 to 32, 0 for no cap.');
245
+ if (value.pauseAt !== undefined && !isInt(value.pauseAt, 0, 100))
246
+ problems.push('pauseAt must be a whole number from 0 to 100, 0 to never hold.');
231
247
  const projects = isObj(value.projects) ? value.projects : null;
232
248
  if (!projects)
233
249
  problems.push('projects must be an object.');
@@ -263,7 +279,7 @@ export function validateConfig(value, saved) {
263
279
  if (!isObj(value.workflows))
264
280
  problems.push('workflows must be an object.');
265
281
  else {
266
- const ctx = { enabled: Array.isArray(value.enabled) ? value.enabled : [], projects: (projects ?? {}) };
282
+ const ctx = { enabled: Array.isArray(value.enabled) ? value.enabled : [], projects: (projects ?? {}), workflows: value.workflows };
267
283
  for (const [name, w] of Object.entries(value.workflows)) {
268
284
  // Folder workflows are checked with their folder, by vsf agents validate.
269
285
  if (isObj(value.folders) && value.folders[name])
@@ -0,0 +1,30 @@
1
+ export declare const looksLikeWebhook: (url: string) => boolean;
2
+ export declare const slug: (name: string) => string;
3
+ export declare const webhookFor: (configDir: string, workflow?: string, platform?: NodeJS.Platform) => string | null;
4
+ export declare function postDiscord(url: string, content: string, fetcher?: typeof fetch, timeoutMs?: number): Promise<string | null>;
5
+ export type RunKind = 'ready' | 'passed' | 'parked' | 'failed';
6
+ export interface RunEvent {
7
+ kind: RunKind;
8
+ card: string;
9
+ title: string;
10
+ workflow: string;
11
+ url?: string;
12
+ prUrl?: string;
13
+ reason?: string;
14
+ }
15
+ export declare function runMessage(e: RunEvent): string;
16
+ export declare function notifyRun(configDir: string, e: RunEvent, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
17
+ export declare function notifyRunner(configDir: string, paused: boolean, reason: string, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
18
+ export interface NotifyState {
19
+ summaryDay?: string;
20
+ summaryAt?: string;
21
+ }
22
+ export declare function readNotifyState(configDir: string): NotifyState;
23
+ export declare function writeNotifyState(configDir: string, state: NotifyState): void;
24
+ export declare function summaryMessage(configDir: string, since: Date, paused: boolean): string | null;
25
+ export declare function sendSummary(configDir: string, paused: boolean, log: (line: string) => void, now?: Date, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<void>;
26
+ export declare function addWebhook(configDir: string, url: string, workflow: string | undefined, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<boolean>;
27
+ export declare function testWebhook(configDir: string, workflow: string | undefined, log: (line: string) => void, fetcher?: typeof fetch, platform?: NodeJS.Platform): Promise<boolean>;
28
+ export declare function removeWebhook(configDir: string, workflow: string | undefined, log: (line: string) => void, platform?: NodeJS.Platform): boolean;
29
+ export declare function moveWebhook(configDir: string, from: string, to: string, platform?: NodeJS.Platform): string | null;
30
+ export declare function dropWebhook(configDir: string, workflow: string, platform?: NodeJS.Platform): void;