stageflow 0.27.0 → 0.30.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 +4 -0
- package/builtin-skills/browser/SKILL.md +42 -0
- package/dist/agent/claudeAdapter.js +2 -2
- package/dist/agent/cursorProvider.d.ts +2 -0
- package/dist/agent/cursorProvider.js +30 -5
- package/dist/agent/fakeAgent.d.ts +1 -0
- package/dist/agent/fakeAgent.js +13 -0
- package/dist/agent/piAdapter.d.ts +3 -24
- package/dist/agent/piAdapter.js +49 -47
- package/dist/agent/piSessionFactory.d.ts +38 -0
- package/dist/agent/piSessionFactory.js +60 -0
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/streamLogRedact.d.ts +6 -0
- package/dist/agent/streamLogRedact.js +23 -1
- package/dist/browser/anchor.d.ts +27 -0
- package/dist/browser/anchor.js +127 -0
- package/dist/browser/auditSink.d.ts +38 -0
- package/dist/browser/auditSink.js +34 -0
- package/dist/browser/browserHost.d.ts +86 -0
- package/dist/browser/browserHost.js +7 -0
- package/dist/browser/browserSweep.d.ts +14 -0
- package/dist/browser/browserSweep.js +54 -0
- package/dist/browser/browserTeardown.d.ts +56 -0
- package/dist/browser/browserTeardown.js +235 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
- package/dist/browser/fakeRemoteBrowserHost.js +34 -0
- package/dist/browser/gateHandoff.d.ts +25 -0
- package/dist/browser/gateHandoff.js +83 -0
- package/dist/browser/humanLogin.d.ts +18 -0
- package/dist/browser/humanLogin.js +34 -0
- package/dist/browser/keyProvider.d.ts +10 -0
- package/dist/browser/keyProvider.js +39 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +152 -0
- package/dist/browser/localProfileLock.d.ts +2 -0
- package/dist/browser/localProfileLock.js +218 -0
- package/dist/browser/localProfileStore.d.ts +5 -0
- package/dist/browser/localProfileStore.js +91 -0
- package/dist/browser/loginCheck.d.ts +53 -0
- package/dist/browser/loginCheck.js +147 -0
- package/dist/browser/memoryProfileLock.d.ts +2 -0
- package/dist/browser/memoryProfileLock.js +53 -0
- package/dist/browser/memoryProfileStore.d.ts +2 -0
- package/dist/browser/memoryProfileStore.js +31 -0
- package/dist/browser/navigationAudit.d.ts +16 -0
- package/dist/browser/navigationAudit.js +53 -0
- package/dist/browser/persistedEnv.d.ts +3 -0
- package/dist/browser/persistedEnv.js +15 -0
- package/dist/browser/profileLock.d.ts +40 -0
- package/dist/browser/profileLock.js +10 -0
- package/dist/browser/profileStore.d.ts +23 -0
- package/dist/browser/profileStore.js +39 -0
- package/dist/browser/runLiveness.d.ts +13 -0
- package/dist/browser/runLiveness.js +39 -0
- package/dist/browser/sitePolicy.d.ts +10 -0
- package/dist/browser/sitePolicy.js +61 -0
- package/dist/browser/stageBrowserEnv.d.ts +22 -0
- package/dist/browser/stageBrowserEnv.js +147 -0
- package/dist/browser/stageProfileLock.d.ts +17 -0
- package/dist/browser/stageProfileLock.js +39 -0
- package/dist/cli/browserCommand.d.ts +44 -0
- package/dist/cli/browserCommand.js +472 -0
- package/dist/cli/hostClient.d.ts +20 -0
- package/dist/cli/hostClient.js +36 -2
- package/dist/cli/initCommand.d.ts +1 -0
- package/dist/cli/initCommand.js +23 -7
- package/dist/cli/initTemplates.d.ts +5 -2
- package/dist/cli/initTemplates.js +25 -2
- package/dist/cli/runsCommand.js +6 -2
- package/dist/cli/triggerCommand.d.ts +12 -0
- package/dist/cli/triggerCommand.js +183 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +23 -6
- package/dist/cli.js +31 -2
- package/dist/config/browseCatalog.d.ts +1 -0
- package/dist/config/browseCatalog.js +5 -4
- package/dist/config/builtinSkills.d.ts +4 -0
- package/dist/config/builtinSkills.js +23 -0
- package/dist/config/catalogRelativePath.d.ts +1 -0
- package/dist/config/catalogRelativePath.js +10 -2
- package/dist/config/createTrigger.d.ts +27 -0
- package/dist/config/createTrigger.js +232 -0
- package/dist/config/draftPackage.d.ts +117 -0
- package/dist/config/draftPackage.js +550 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
- package/dist/config/ensureCatalogScanRoot.js +73 -0
- package/dist/config/hostConfig.d.ts +2 -0
- package/dist/config/hostConfig.js +27 -0
- package/dist/config/listSkills.d.ts +2 -2
- package/dist/config/listSkills.js +17 -3
- package/dist/config/loadOutcome.d.ts +6 -0
- package/dist/config/loadPipeline.js +16 -0
- package/dist/config/loadStage.js +7 -0
- package/dist/config/loadStageflowManifest.js +25 -0
- package/dist/config/loadTrigger.d.ts +9 -0
- package/dist/config/loadTrigger.js +120 -0
- package/dist/config/multiProjectCatalog.d.ts +2 -0
- package/dist/config/multiProjectCatalog.js +26 -18
- package/dist/config/normalizePipelineStageEntry.js +8 -1
- package/dist/config/parseCompletionContract.js +6 -0
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/publishDraftPackage.d.ts +7 -0
- package/dist/config/publishDraftPackage.js +25 -0
- package/dist/config/resolveCatalogContext.js +1 -1
- package/dist/config/resolvePipelineDag.js +1 -1
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +6 -2
- package/dist/config/stageBrowser.d.ts +12 -0
- package/dist/config/stageBrowser.js +118 -0
- package/dist/config/triggerCatalog.d.ts +10 -0
- package/dist/config/triggerCatalog.js +37 -0
- package/dist/config/validateCatalog.d.ts +13 -3
- package/dist/config/validateCatalog.js +83 -2
- package/dist/config/validateCompletionContract.js +8 -0
- package/dist/envelope/preEmitChecks.d.ts +1 -1
- package/dist/envelope/preEmitChecks.js +8 -0
- package/dist/logging/redact.d.ts +4 -0
- package/dist/logging/redact.js +17 -0
- package/dist/mcp/catalogTools.js +43 -8
- package/dist/mcp/controlTools.js +3 -1
- package/dist/mcp/deps.d.ts +2 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +2 -0
- package/dist/mcp/triggerTools.d.ts +3 -0
- package/dist/mcp/triggerTools.js +176 -0
- package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
- package/dist/operatorAgent/docsRetrieval.js +269 -0
- package/dist/operatorAgent/draftContext.d.ts +16 -0
- package/dist/operatorAgent/draftContext.js +68 -0
- package/dist/operatorAgent/fakeHost.d.ts +39 -0
- package/dist/operatorAgent/fakeHost.js +92 -0
- package/dist/operatorAgent/host.d.ts +40 -0
- package/dist/operatorAgent/host.js +179 -0
- package/dist/operatorAgent/index.d.ts +9 -0
- package/dist/operatorAgent/index.js +8 -0
- package/dist/operatorAgent/piModel.d.ts +80 -0
- package/dist/operatorAgent/piModel.js +528 -0
- package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
- package/dist/operatorAgent/proposals.d.ts +32 -0
- package/dist/operatorAgent/proposals.js +144 -0
- package/dist/operatorAgent/types.d.ts +128 -0
- package/dist/operatorAgent/types.js +1 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/project/findManifestRoot.d.ts +3 -0
- package/dist/project/findManifestRoot.js +35 -0
- package/dist/project/resolveStageflowContext.d.ts +7 -0
- package/dist/project/resolveStageflowContext.js +44 -28
- package/dist/runstore/assertTmpdir.js +6 -2
- package/dist/runstore/port.d.ts +28 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
- package/dist/runstore/sqlite/migrations/index.js +6 -0
- package/dist/runtime/answerResume.d.ts +1 -0
- package/dist/runtime/answerResume.js +3 -1
- package/dist/runtime/completionCheckRunner.d.ts +12 -1
- package/dist/runtime/completionCheckRunner.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +2 -0
- package/dist/runtime/credentialBinding.js +3 -0
- package/dist/runtime/emailSource.d.ts +72 -0
- package/dist/runtime/emailSource.js +304 -0
- package/dist/runtime/githubPollSource.d.ts +86 -0
- package/dist/runtime/githubPollSource.js +284 -0
- package/dist/runtime/pipelineRunner.d.ts +4 -0
- package/dist/runtime/pipelineRunner.js +3 -0
- package/dist/runtime/pipelineScheduler.d.ts +2 -0
- package/dist/runtime/pipelineScheduler.js +85 -1
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/runManager.d.ts +11 -0
- package/dist/runtime/runManager.js +149 -1
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +1 -0
- package/dist/runtime/scheduleSource.d.ts +38 -0
- package/dist/runtime/scheduleSource.js +119 -0
- package/dist/runtime/settingsFile.d.ts +5 -0
- package/dist/runtime/settingsFile.js +61 -54
- package/dist/runtime/stageAttemptBootstrap.js +55 -3
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageHitl.d.ts +1 -0
- package/dist/runtime/stageHitl.js +5 -1
- package/dist/runtime/stageProcessLauncher.d.ts +2 -0
- package/dist/runtime/stageProcessLauncher.js +44 -5
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +30 -6
- package/dist/runtime/stageStreamLog.d.ts +1 -0
- package/dist/runtime/stageStreamLog.js +2 -1
- package/dist/runtime/triggerCatalog.d.ts +6 -0
- package/dist/runtime/triggerCatalog.js +24 -0
- package/dist/runtime/triggerEventMatch.d.ts +2 -0
- package/dist/runtime/triggerEventMatch.js +6 -0
- package/dist/runtime/triggerPort.d.ts +14 -0
- package/dist/runtime/triggerPort.js +1 -0
- package/dist/runtime/triggerRunner.d.ts +17 -0
- package/dist/runtime/triggerRunner.js +89 -0
- package/dist/runtime/verifiedStageExecution.d.ts +7 -0
- package/dist/runtime/verifiedStageExecution.js +14 -0
- package/dist/runtime/webhookSignature.d.ts +1 -0
- package/dist/runtime/webhookSignature.js +12 -0
- package/dist/server/bootstrap.d.ts +6 -0
- package/dist/server/bootstrap.js +64 -0
- package/dist/server/controlToken.js +13 -0
- package/dist/server/createHttpHost.js +3 -0
- package/dist/server/healthSurfaces.js +31 -6
- package/dist/server/http.d.ts +15 -0
- package/dist/server/http.js +1251 -74
- package/dist/server/requestAuthContext.d.ts +1 -1
- package/dist/server/triggerRoutes.d.ts +15 -0
- package/dist/server/triggerRoutes.js +268 -0
- package/dist/tools/askOperator.d.ts +3 -2
- package/dist/types/completion.d.ts +3 -0
- package/dist/types/pipeline.d.ts +3 -1
- package/dist/types/preEmitCheck.d.ts +7 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +3 -0
- package/dist/types/trigger.d.ts +18 -0
- package/dist/types/trigger.js +1 -0
- package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
- package/dist/ui/assets/index-CntB4bZR.js +133 -0
- package/dist/ui/index.html +2 -2
- package/dist/workshop/autosave.d.ts +58 -0
- package/dist/workshop/autosave.js +222 -0
- package/dist/workshop/buildStore.d.ts +79 -0
- package/dist/workshop/buildStore.js +326 -0
- package/dist/workshop/chatTurn.d.ts +232 -0
- package/dist/workshop/chatTurn.js +475 -0
- package/dist/workshop/modelSettings.d.ts +18 -0
- package/dist/workshop/modelSettings.js +44 -0
- package/dist/workshop/sessionStore.d.ts +61 -0
- package/dist/workshop/sessionStore.js +254 -0
- package/dist/workshop/workshopPin.d.ts +72 -0
- package/dist/workshop/workshopPin.js +193 -0
- package/examples/README.md +2 -1
- package/examples/browser-session/README.md +77 -0
- package/examples/browser-session/browser-session.pipeline.yaml +50 -0
- package/examples/browser-session/browser-session.task.yaml +6 -0
- package/examples/browser-session/check-login.yaml +22 -0
- package/examples/browser-session/fixture-server.mjs +64 -0
- package/examples/browser-session/human-login.yaml +31 -0
- package/examples/browser-session/merge.yaml +36 -0
- package/examples/browser-session/work-a.yaml +41 -0
- package/examples/browser-session/work-b.yaml +41 -0
- package/examples/stage-mcp/README.md +3 -2
- package/examples/stageflow.yaml +51 -0
- package/package.json +5 -1
- package/skills/stageflow/references/control-surface.md +3 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
- package/skills/stageflow-run/SKILL.md +2 -0
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
- package/skills/stageflow-setup/SKILL.md +11 -0
- package/dist/ui/assets/index-B6hlSXXn.js +0 -120
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../../../package-meta.js";
|
|
2
|
+
function addNextRunAtColumn(db) {
|
|
3
|
+
try {
|
|
4
|
+
db.exec(`ALTER TABLE triggers ADD COLUMN next_run_at TEXT`);
|
|
5
|
+
}
|
|
6
|
+
catch (err) {
|
|
7
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8
|
+
if (!/duplicate column name/i.test(message))
|
|
9
|
+
throw err;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const MIGRATION_009 = {
|
|
13
|
+
version: 9,
|
|
14
|
+
name: "009_trigger_next_run",
|
|
15
|
+
minStageflowVersion: PACKAGE_VERSION,
|
|
16
|
+
up(db) {
|
|
17
|
+
addNextRunAtColumn(db);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../../../package-meta.js";
|
|
2
|
+
export const MIGRATION_010 = {
|
|
3
|
+
version: 10,
|
|
4
|
+
name: "010_trigger_adapter_state",
|
|
5
|
+
minStageflowVersion: PACKAGE_VERSION,
|
|
6
|
+
up(db) {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS trigger_adapter_state (
|
|
9
|
+
trigger_id TEXT NOT NULL,
|
|
10
|
+
key TEXT NOT NULL,
|
|
11
|
+
value TEXT NOT NULL,
|
|
12
|
+
updated_at TEXT NOT NULL,
|
|
13
|
+
PRIMARY KEY (trigger_id, key)
|
|
14
|
+
);
|
|
15
|
+
`);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -6,6 +6,9 @@ import { MIGRATION_004 } from "./004-auto-resume-count.js";
|
|
|
6
6
|
import { MIGRATION_005 } from "./005-config-origins.js";
|
|
7
7
|
import { MIGRATION_006 } from "./006-pipeline-body-and-caller.js";
|
|
8
8
|
import { MIGRATION_007 } from "./007-projects-registry.js";
|
|
9
|
+
import { MIGRATION_008 } from "./008-triggers-table.js";
|
|
10
|
+
import { MIGRATION_009 } from "./009-trigger-next-run.js";
|
|
11
|
+
import { MIGRATION_010 } from "./010-trigger-adapter-state.js";
|
|
9
12
|
const DEFAULT_MIGRATIONS = [
|
|
10
13
|
MIGRATION_001,
|
|
11
14
|
MIGRATION_002,
|
|
@@ -14,6 +17,9 @@ const DEFAULT_MIGRATIONS = [
|
|
|
14
17
|
MIGRATION_005,
|
|
15
18
|
MIGRATION_006,
|
|
16
19
|
MIGRATION_007,
|
|
20
|
+
MIGRATION_008,
|
|
21
|
+
MIGRATION_009,
|
|
22
|
+
MIGRATION_010,
|
|
17
23
|
];
|
|
18
24
|
export const CURRENT_SCHEMA_VERSION = DEFAULT_MIGRATIONS[DEFAULT_MIGRATIONS.length - 1].version;
|
|
19
25
|
function throwStoreSchemaTooNew(db, version, maxKnown) {
|
|
@@ -4,6 +4,7 @@ export declare const WAIT_WITHOUT_WORKER_DISPATCH = "stage entered wait without
|
|
|
4
4
|
export type AnswerResumeAdapterResult = {
|
|
5
5
|
ok: boolean;
|
|
6
6
|
reason?: string;
|
|
7
|
+
verification?: "failed_retrying";
|
|
7
8
|
};
|
|
8
9
|
export declare function orchestrateAnswerResume(options: {
|
|
9
10
|
prefixMode: "live" | "restart";
|
|
@@ -72,6 +72,12 @@ export type CompletionCheckRunnerInput = {
|
|
|
72
72
|
/** Why the runtime could not establish the required pre-stage baseline. */
|
|
73
73
|
checkoutError?: string;
|
|
74
74
|
checklistAttestations?: readonly ChecklistAttestation[];
|
|
75
|
+
/** Re-runs the Host login check against the stage's own browser session. */
|
|
76
|
+
browserLogin?: () => Promise<{
|
|
77
|
+
state: "logged_in" | "logged_out" | "unknown";
|
|
78
|
+
logged_in: boolean | null;
|
|
79
|
+
url: string;
|
|
80
|
+
}>;
|
|
75
81
|
onCheckStart?: (check: CompletionCheck) => Promise<void> | void;
|
|
76
82
|
onCheckComplete?: (result: CompletionCheckResult) => Promise<void> | void;
|
|
77
83
|
};
|
|
@@ -118,7 +124,12 @@ export type ChecklistCheckEvidence = {
|
|
|
118
124
|
missing_items: string[];
|
|
119
125
|
unexpected_items: string[];
|
|
120
126
|
};
|
|
121
|
-
export type
|
|
127
|
+
export type BrowserLoginCheckEvidence = {
|
|
128
|
+
kind: "browser_login";
|
|
129
|
+
state?: "logged_in" | "logged_out" | "unknown";
|
|
130
|
+
url?: string;
|
|
131
|
+
};
|
|
132
|
+
export type CompletionCheckEvidence = BrowserLoginCheckEvidence | CommandCheckEvidence | ArtifactCheckEvidence | PayloadSchemaCheckEvidence | GateCheckEvidence | CheckoutChangesCheckEvidence | ChecklistCheckEvidence;
|
|
122
133
|
export type CompletionCheckResult = {
|
|
123
134
|
check_id: string;
|
|
124
135
|
check_type: CompletionCheck["type"];
|
|
@@ -364,6 +364,26 @@ function runChecklistCheck(check, input) {
|
|
|
364
364
|
}
|
|
365
365
|
return result(check, "passed", evidence);
|
|
366
366
|
}
|
|
367
|
+
async function runBrowserLoginCheck(check, input) {
|
|
368
|
+
const empty = { kind: "browser_login" };
|
|
369
|
+
if (input.browserLogin === undefined) {
|
|
370
|
+
return result(check, "error", empty, "browser_login check requires a browser session");
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const observed = await input.browserLogin();
|
|
374
|
+
const evidence = {
|
|
375
|
+
kind: "browser_login",
|
|
376
|
+
state: observed.state,
|
|
377
|
+
url: observed.url,
|
|
378
|
+
};
|
|
379
|
+
return observed.logged_in === true
|
|
380
|
+
? result(check, "passed", evidence)
|
|
381
|
+
: result(check, "failed", evidence, `the Host login check still sees a ${observed.state === "unknown" ? "session it cannot confirm" : "logged-out session"}`);
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return result(check, "error", empty, error instanceof Error ? error.message : String(error));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
367
387
|
async function runCheck(check, input) {
|
|
368
388
|
switch (check.type) {
|
|
369
389
|
case "command":
|
|
@@ -378,6 +398,8 @@ async function runCheck(check, input) {
|
|
|
378
398
|
return runCheckoutChangesCheck(check, input);
|
|
379
399
|
case "checklist":
|
|
380
400
|
return runChecklistCheck(check, input);
|
|
401
|
+
case "browser_login":
|
|
402
|
+
return runBrowserLoginCheck(check, input);
|
|
381
403
|
}
|
|
382
404
|
}
|
|
383
405
|
/**
|
|
@@ -11,6 +11,8 @@ export type ResolveCredentialBindingOptions = {
|
|
|
11
11
|
};
|
|
12
12
|
export declare function sfOwnedAgentDir(): string;
|
|
13
13
|
export declare function sfOwnedAuthPath(): string;
|
|
14
|
+
export declare const PI_HOME_AUTH_PATH_ENV = "STAGEFLOW_PI_HOME_AUTH_PATH";
|
|
15
|
+
/** A stage worker's HOME is an empty attempt dir, so the Host passes its own Pi auth path through the environment. */
|
|
14
16
|
export declare function piHomeAuthPath(override?: string): string;
|
|
15
17
|
export declare function isUsableAuthFile(authPath: string): boolean;
|
|
16
18
|
export declare function ensureSfOwnedAuthStore(): string;
|
|
@@ -11,8 +11,11 @@ export function sfOwnedAgentDir() {
|
|
|
11
11
|
export function sfOwnedAuthPath() {
|
|
12
12
|
return path.join(sfOwnedAgentDir(), "auth.json");
|
|
13
13
|
}
|
|
14
|
+
export const PI_HOME_AUTH_PATH_ENV = "STAGEFLOW_PI_HOME_AUTH_PATH";
|
|
15
|
+
/** A stage worker's HOME is an empty attempt dir, so the Host passes its own Pi auth path through the environment. */
|
|
14
16
|
export function piHomeAuthPath(override) {
|
|
15
17
|
return (override ??
|
|
18
|
+
(process.env[PI_HOME_AUTH_PATH_ENV] || undefined) ??
|
|
16
19
|
path.join(os.homedir(), ".pi", "agent", "auth.json"));
|
|
17
20
|
}
|
|
18
21
|
export function isUsableAuthFile(authPath) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { RunStore } from "../runstore/port.js";
|
|
2
|
+
import type { TriggerFireEvent, TriggerSourcePort } from "./triggerPort.js";
|
|
3
|
+
export declare const DEFAULT_EMAIL_RECONNECT_BASE_DELAY_MS = 1000;
|
|
4
|
+
export declare const DEFAULT_EMAIL_RECONNECT_MAX_DELAY_MS = 30000;
|
|
5
|
+
export declare const EMAIL_TEXT_EXCERPT_MAX_CHARS = 2000;
|
|
6
|
+
export type NormalizedEmailMessage = {
|
|
7
|
+
uid: number;
|
|
8
|
+
from: string;
|
|
9
|
+
subject: string;
|
|
10
|
+
date: string;
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
export type EmailClientConfig = {
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
user: string;
|
|
17
|
+
password: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Narrow, test-friendly seam over "one IMAP/IDLE mailbox connection". The
|
|
21
|
+
* default implementation adapts `imapflow`'s `ImapFlow`; tests inject a fake
|
|
22
|
+
* so no real mailbox is ever contacted.
|
|
23
|
+
*/
|
|
24
|
+
export type EmailImapClient = {
|
|
25
|
+
connect(): Promise<{
|
|
26
|
+
uidNext: number;
|
|
27
|
+
}>;
|
|
28
|
+
idle(): void;
|
|
29
|
+
fetchNewMessages(sinceUid: number): Promise<NormalizedEmailMessage[]>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
onExists(listener: () => void): void;
|
|
32
|
+
onClose(listener: () => void): void;
|
|
33
|
+
onError(listener: (err: Error) => void): void;
|
|
34
|
+
};
|
|
35
|
+
export type EmailSourceOptions = {
|
|
36
|
+
store: RunStore;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
logError?: (message: string) => void;
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
/** Injectable client factory so tests never hit a real mailbox. */
|
|
41
|
+
createEmailClient?: (config: EmailClientConfig) => EmailImapClient;
|
|
42
|
+
reconnectBaseDelayMs?: number;
|
|
43
|
+
reconnectMaxDelayMs?: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* `TriggerSourcePort` adapter for `event`-kind triggers whose `event.source`
|
|
47
|
+
* starts with `email.`. Unlike the poll-based adapters this one is push-based:
|
|
48
|
+
* on `start()` it groups triggers by distinct mailbox (`host`+`port`+`user`),
|
|
49
|
+
* opens exactly one real IMAP/IDLE connection per mailbox regardless of how
|
|
50
|
+
* many triggers watch it, and reacts to the server's new-message
|
|
51
|
+
* notification instead of ticking on an interval. A dropped connection
|
|
52
|
+
* reconnects with bounded exponential backoff rather than going dark.
|
|
53
|
+
*/
|
|
54
|
+
export declare class EmailSource implements TriggerSourcePort {
|
|
55
|
+
private readonly store;
|
|
56
|
+
private readonly cwd;
|
|
57
|
+
private readonly logError;
|
|
58
|
+
private readonly env;
|
|
59
|
+
private readonly createEmailClient;
|
|
60
|
+
private readonly reconnectBaseDelayMs;
|
|
61
|
+
private readonly reconnectMaxDelayMs;
|
|
62
|
+
private readonly connections;
|
|
63
|
+
private stopped;
|
|
64
|
+
constructor(options: EmailSourceOptions);
|
|
65
|
+
start(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
|
|
66
|
+
stop(): Promise<void>;
|
|
67
|
+
private discover;
|
|
68
|
+
private connectMailbox;
|
|
69
|
+
private scheduleReconnect;
|
|
70
|
+
private processNewMail;
|
|
71
|
+
private fireSafely;
|
|
72
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ImapFlow } from "imapflow";
|
|
3
|
+
import { getCatalogScanPaths } from "../config/browseCatalog.js";
|
|
4
|
+
import { loadTriggerOutcome } from "../config/loadTrigger.js";
|
|
5
|
+
import { catalogContextFromStageflow } from "../config/resolveCatalogContext.js";
|
|
6
|
+
import { readSecretFromEnvOrFile } from "../config/secretFromEnvOrFile.js";
|
|
7
|
+
import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
|
|
8
|
+
import { matchesEventFilter } from "./triggerEventMatch.js";
|
|
9
|
+
export const DEFAULT_EMAIL_RECONNECT_BASE_DELAY_MS = 1_000;
|
|
10
|
+
export const DEFAULT_EMAIL_RECONNECT_MAX_DELAY_MS = 30_000;
|
|
11
|
+
export const EMAIL_TEXT_EXCERPT_MAX_CHARS = 2_000;
|
|
12
|
+
function defaultCreateEmailClient(config) {
|
|
13
|
+
const client = new ImapFlow({
|
|
14
|
+
host: config.host,
|
|
15
|
+
port: config.port,
|
|
16
|
+
secure: config.port === 993 || config.port === 465,
|
|
17
|
+
auth: { user: config.user, pass: config.password },
|
|
18
|
+
logger: false,
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
async connect() {
|
|
22
|
+
await client.connect();
|
|
23
|
+
const mailbox = await client.mailboxOpen("INBOX");
|
|
24
|
+
return { uidNext: mailbox.uidNext };
|
|
25
|
+
},
|
|
26
|
+
idle() {
|
|
27
|
+
void client.idle().catch(() => {
|
|
28
|
+
// Connection drop surfaces via the 'close'/'error' events below.
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
async fetchNewMessages(sinceUid) {
|
|
32
|
+
if (sinceUid <= 0)
|
|
33
|
+
return [];
|
|
34
|
+
const results = [];
|
|
35
|
+
for await (const msg of client.fetch(`${sinceUid + 1}:*`, { uid: true, envelope: true, bodyParts: ["TEXT"] }, { uid: true })) {
|
|
36
|
+
if (msg.uid <= sinceUid)
|
|
37
|
+
continue;
|
|
38
|
+
const fromAddr = msg.envelope?.from?.[0];
|
|
39
|
+
const from = fromAddr?.address ?? fromAddr?.name ?? "";
|
|
40
|
+
const subject = msg.envelope?.subject ?? "";
|
|
41
|
+
const rawDate = msg.envelope?.date;
|
|
42
|
+
const date = rawDate instanceof Date ? rawDate.toISOString() : String(rawDate ?? "");
|
|
43
|
+
const text = msg.bodyParts?.get("TEXT")?.toString("utf8").slice(0, EMAIL_TEXT_EXCERPT_MAX_CHARS) ?? "";
|
|
44
|
+
results.push({ uid: msg.uid, from, subject, date, text });
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
},
|
|
48
|
+
async close() {
|
|
49
|
+
try {
|
|
50
|
+
await client.logout();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
client.close();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
onExists(listener) {
|
|
57
|
+
client.on("exists", () => listener());
|
|
58
|
+
},
|
|
59
|
+
onClose(listener) {
|
|
60
|
+
client.on("close", () => listener());
|
|
61
|
+
},
|
|
62
|
+
onError(listener) {
|
|
63
|
+
client.on("error", (err) => listener(err));
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseMailboxConfig(definition) {
|
|
68
|
+
const config = definition.event?.config;
|
|
69
|
+
const host = config?.host;
|
|
70
|
+
const port = config?.port;
|
|
71
|
+
const user = config?.user;
|
|
72
|
+
const secretRef = config?.secretRef;
|
|
73
|
+
if (typeof host !== "string" ||
|
|
74
|
+
host.length === 0 ||
|
|
75
|
+
typeof port !== "number" ||
|
|
76
|
+
!Number.isFinite(port) ||
|
|
77
|
+
typeof user !== "string" ||
|
|
78
|
+
user.length === 0 ||
|
|
79
|
+
typeof secretRef !== "string" ||
|
|
80
|
+
secretRef.length === 0) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return { host, port, user, secretRef };
|
|
84
|
+
}
|
|
85
|
+
function mailboxKeyFor(config) {
|
|
86
|
+
return `email:${config.host}:${config.port}:${config.user}`;
|
|
87
|
+
}
|
|
88
|
+
function errMsg(err) {
|
|
89
|
+
return err instanceof Error ? err.message : String(err);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `TriggerSourcePort` adapter for `event`-kind triggers whose `event.source`
|
|
93
|
+
* starts with `email.`. Unlike the poll-based adapters this one is push-based:
|
|
94
|
+
* on `start()` it groups triggers by distinct mailbox (`host`+`port`+`user`),
|
|
95
|
+
* opens exactly one real IMAP/IDLE connection per mailbox regardless of how
|
|
96
|
+
* many triggers watch it, and reacts to the server's new-message
|
|
97
|
+
* notification instead of ticking on an interval. A dropped connection
|
|
98
|
+
* reconnects with bounded exponential backoff rather than going dark.
|
|
99
|
+
*/
|
|
100
|
+
export class EmailSource {
|
|
101
|
+
store;
|
|
102
|
+
cwd;
|
|
103
|
+
logError;
|
|
104
|
+
env;
|
|
105
|
+
createEmailClient;
|
|
106
|
+
reconnectBaseDelayMs;
|
|
107
|
+
reconnectMaxDelayMs;
|
|
108
|
+
connections = new Map();
|
|
109
|
+
stopped = false;
|
|
110
|
+
constructor(options) {
|
|
111
|
+
this.store = options.store;
|
|
112
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
113
|
+
this.logError =
|
|
114
|
+
options.logError ??
|
|
115
|
+
((message) => {
|
|
116
|
+
console.error(message);
|
|
117
|
+
});
|
|
118
|
+
this.env = options.env ?? process.env;
|
|
119
|
+
this.createEmailClient = options.createEmailClient ?? defaultCreateEmailClient;
|
|
120
|
+
this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? DEFAULT_EMAIL_RECONNECT_BASE_DELAY_MS;
|
|
121
|
+
this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? DEFAULT_EMAIL_RECONNECT_MAX_DELAY_MS;
|
|
122
|
+
}
|
|
123
|
+
async start(onFire) {
|
|
124
|
+
this.stopped = false;
|
|
125
|
+
const ctx = catalogContextFromStageflow(await resolveStageflowContext(this.cwd));
|
|
126
|
+
const scanPaths = await getCatalogScanPaths(ctx);
|
|
127
|
+
if (!scanPaths)
|
|
128
|
+
return;
|
|
129
|
+
const projectRoot = ctx.projectRoot ?? undefined;
|
|
130
|
+
const groups = await this.discover(scanPaths.triggerPaths);
|
|
131
|
+
for (const [mailboxKey, group] of groups) {
|
|
132
|
+
for (const match of group.matches) {
|
|
133
|
+
const definitionRef = projectRoot !== undefined
|
|
134
|
+
? path.relative(projectRoot, match.path).replace(/\\/g, "/")
|
|
135
|
+
: match.path;
|
|
136
|
+
await this.store.upsertTrigger({
|
|
137
|
+
id: match.definition.id,
|
|
138
|
+
definitionRef,
|
|
139
|
+
enabled: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
await this.connectMailbox(mailboxKey, group, onFire);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async stop() {
|
|
146
|
+
this.stopped = true;
|
|
147
|
+
for (const [mailboxKey, conn] of this.connections) {
|
|
148
|
+
conn.stopping = true;
|
|
149
|
+
if (conn.reconnectTimer !== undefined) {
|
|
150
|
+
clearTimeout(conn.reconnectTimer);
|
|
151
|
+
conn.reconnectTimer = undefined;
|
|
152
|
+
}
|
|
153
|
+
if (conn.client) {
|
|
154
|
+
try {
|
|
155
|
+
await conn.client.close();
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
this.logError(`email: closing mailbox "${mailboxKey}" failed: ${errMsg(err)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.connections.clear();
|
|
163
|
+
}
|
|
164
|
+
async discover(triggerPaths) {
|
|
165
|
+
const groups = new Map();
|
|
166
|
+
for (const triggerPath of triggerPaths) {
|
|
167
|
+
const outcome = await loadTriggerOutcome(triggerPath);
|
|
168
|
+
if (!outcome.ok)
|
|
169
|
+
continue;
|
|
170
|
+
const definition = outcome.value;
|
|
171
|
+
if (definition.kind !== "event" || !definition.enabled)
|
|
172
|
+
continue;
|
|
173
|
+
const source = definition.event?.source;
|
|
174
|
+
if (typeof source !== "string" || !source.startsWith("email."))
|
|
175
|
+
continue;
|
|
176
|
+
const config = parseMailboxConfig(definition);
|
|
177
|
+
if (!config) {
|
|
178
|
+
this.logError(`email: trigger "${definition.id}" has incomplete event.config ` +
|
|
179
|
+
`(expected host, port, user, secretRef); skipping`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const mailboxKey = mailboxKeyFor(config);
|
|
183
|
+
const group = groups.get(mailboxKey) ?? { config, matches: [] };
|
|
184
|
+
group.matches.push({ path: triggerPath, definition });
|
|
185
|
+
groups.set(mailboxKey, group);
|
|
186
|
+
}
|
|
187
|
+
return groups;
|
|
188
|
+
}
|
|
189
|
+
async connectMailbox(mailboxKey, group, onFire) {
|
|
190
|
+
let conn = this.connections.get(mailboxKey);
|
|
191
|
+
if (!conn) {
|
|
192
|
+
conn = { group, onFire, reconnectAttempt: 0, stopping: false };
|
|
193
|
+
this.connections.set(mailboxKey, conn);
|
|
194
|
+
}
|
|
195
|
+
if (conn.stopping || this.stopped)
|
|
196
|
+
return;
|
|
197
|
+
let password;
|
|
198
|
+
try {
|
|
199
|
+
password = readSecretFromEnvOrFile(this.env, group.config.secretRef);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
this.logError(`email: failed to read secret "${group.config.secretRef}" for mailbox "${mailboxKey}": ${errMsg(err)}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (password === undefined) {
|
|
206
|
+
this.logError(`email: secret "${group.config.secretRef}" is not set for mailbox "${mailboxKey}"; skipping`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const client = this.createEmailClient({
|
|
210
|
+
host: group.config.host,
|
|
211
|
+
port: group.config.port,
|
|
212
|
+
user: group.config.user,
|
|
213
|
+
password,
|
|
214
|
+
});
|
|
215
|
+
conn.client = client;
|
|
216
|
+
client.onError((err) => {
|
|
217
|
+
this.logError(`email: mailbox "${mailboxKey}" error: ${errMsg(err)}`);
|
|
218
|
+
});
|
|
219
|
+
client.onClose(() => {
|
|
220
|
+
const current = this.connections.get(mailboxKey);
|
|
221
|
+
if (this.stopped || !current || current.stopping)
|
|
222
|
+
return;
|
|
223
|
+
this.scheduleReconnect(mailboxKey);
|
|
224
|
+
});
|
|
225
|
+
client.onExists(() => {
|
|
226
|
+
void this.processNewMail(mailboxKey).catch((err) => {
|
|
227
|
+
this.logError(`email: processing new mail for mailbox "${mailboxKey}" failed: ${errMsg(err)}`);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
const { uidNext } = await client.connect();
|
|
232
|
+
const existing = await this.store.getTriggerAdapterState(mailboxKey, "uid");
|
|
233
|
+
if (existing === null) {
|
|
234
|
+
await this.store.setTriggerAdapterState(mailboxKey, "uid", String(Math.max(uidNext - 1, 0)));
|
|
235
|
+
}
|
|
236
|
+
client.idle();
|
|
237
|
+
conn.reconnectAttempt = 0;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
this.logError(`email: connecting to mailbox "${mailboxKey}" failed: ${errMsg(err)}`);
|
|
241
|
+
this.scheduleReconnect(mailboxKey);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
scheduleReconnect(mailboxKey) {
|
|
245
|
+
const conn = this.connections.get(mailboxKey);
|
|
246
|
+
if (!conn || conn.stopping || this.stopped)
|
|
247
|
+
return;
|
|
248
|
+
const attempt = conn.reconnectAttempt;
|
|
249
|
+
const delay = Math.min(this.reconnectBaseDelayMs * 2 ** attempt, this.reconnectMaxDelayMs);
|
|
250
|
+
conn.reconnectAttempt = attempt + 1;
|
|
251
|
+
conn.reconnectTimer = setTimeout(() => {
|
|
252
|
+
conn.reconnectTimer = undefined;
|
|
253
|
+
void this.connectMailbox(mailboxKey, conn.group, conn.onFire);
|
|
254
|
+
}, delay).unref();
|
|
255
|
+
}
|
|
256
|
+
async processNewMail(mailboxKey) {
|
|
257
|
+
const conn = this.connections.get(mailboxKey);
|
|
258
|
+
if (!conn?.client)
|
|
259
|
+
return;
|
|
260
|
+
const watermarkRaw = await this.store.getTriggerAdapterState(mailboxKey, "uid");
|
|
261
|
+
const sinceUid = watermarkRaw !== null ? Number(watermarkRaw) : 0;
|
|
262
|
+
const messages = await conn.client.fetchNewMessages(sinceUid);
|
|
263
|
+
if (messages.length === 0)
|
|
264
|
+
return;
|
|
265
|
+
let maxUid = sinceUid;
|
|
266
|
+
for (const message of messages) {
|
|
267
|
+
if (message.uid <= sinceUid)
|
|
268
|
+
continue;
|
|
269
|
+
if (message.uid > maxUid)
|
|
270
|
+
maxUid = message.uid;
|
|
271
|
+
const summary = {
|
|
272
|
+
from: message.from,
|
|
273
|
+
subject: message.subject,
|
|
274
|
+
date: message.date,
|
|
275
|
+
text: message.text,
|
|
276
|
+
};
|
|
277
|
+
for (const { definition } of conn.group.matches) {
|
|
278
|
+
if (!matchesEventFilter(definition.event?.match, summary))
|
|
279
|
+
continue;
|
|
280
|
+
await this.fireSafely(conn.onFire, definition, message, summary);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (maxUid > sinceUid) {
|
|
284
|
+
await this.store.setTriggerAdapterState(mailboxKey, "uid", String(maxUid));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async fireSafely(onFire, definition, message, summary) {
|
|
288
|
+
try {
|
|
289
|
+
if (definition.task !== undefined) {
|
|
290
|
+
await onFire({ triggerId: definition.id });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const task = {
|
|
294
|
+
id: `${definition.id}-email-${message.uid}`,
|
|
295
|
+
goal: `Handle email: ${message.subject}`,
|
|
296
|
+
input: summary,
|
|
297
|
+
};
|
|
298
|
+
await onFire({ triggerId: definition.id, task });
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
this.logError(`email: firing trigger "${definition.id}" failed: ${errMsg(err)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { RunStore } from "../runstore/port.js";
|
|
2
|
+
import type { TriggerFireEvent, TriggerSourcePort } from "./triggerPort.js";
|
|
3
|
+
export declare const DEFAULT_GITHUB_POLL_INTERVAL_MS = 60000;
|
|
4
|
+
export declare const DEFAULT_GITHUB_TOKEN_SECRET = "GITHUB_TOKEN";
|
|
5
|
+
/** Parse `STAGEFLOW_GITHUB_POLL_INTERVAL_MS`; default 60s; `0` disables. */
|
|
6
|
+
export declare function githubPollIntervalMsFromEnv(env?: NodeJS.ProcessEnv): number;
|
|
7
|
+
export type GithubPullItem = {
|
|
8
|
+
number: number;
|
|
9
|
+
title: string;
|
|
10
|
+
html_url: string;
|
|
11
|
+
state: string;
|
|
12
|
+
merged_at: string | null;
|
|
13
|
+
created_at: string;
|
|
14
|
+
updated_at: string;
|
|
15
|
+
user: {
|
|
16
|
+
login: string;
|
|
17
|
+
} | null;
|
|
18
|
+
};
|
|
19
|
+
export type GithubPollResponse = {
|
|
20
|
+
status: number;
|
|
21
|
+
etag?: string;
|
|
22
|
+
items: GithubPullItem[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Narrow, test-friendly seam over "list PR activity for a repo, conditionally
|
|
26
|
+
* on an ETag". The default implementation adapts `octokit.rest.pulls.list`;
|
|
27
|
+
* tests inject a fake so no real GitHub calls happen.
|
|
28
|
+
*/
|
|
29
|
+
export type GithubPollClient = {
|
|
30
|
+
listPulls(params: {
|
|
31
|
+
owner: string;
|
|
32
|
+
repo: string;
|
|
33
|
+
etag?: string;
|
|
34
|
+
}): Promise<GithubPollResponse>;
|
|
35
|
+
};
|
|
36
|
+
export declare function defaultCreateGithubClient(token: string): GithubPollClient;
|
|
37
|
+
export type NormalizedGithubEvent = {
|
|
38
|
+
action: "opened" | "merged" | "closed" | "updated";
|
|
39
|
+
number: number;
|
|
40
|
+
title: string;
|
|
41
|
+
url: string;
|
|
42
|
+
author: string;
|
|
43
|
+
repo: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
};
|
|
46
|
+
export type GithubPollSourceOptions = {
|
|
47
|
+
store: RunStore;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
intervalMs?: number;
|
|
50
|
+
logError?: (message: string) => void;
|
|
51
|
+
env?: NodeJS.ProcessEnv;
|
|
52
|
+
/** Injectable client factory so tests never hit real GitHub. */
|
|
53
|
+
createGithubClient?: (token: string) => GithubPollClient;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* `TriggerSourcePort` adapter for `event`-kind triggers whose `event.source`
|
|
57
|
+
* starts with `github.`. Ticks on an interval; each tick re-scans the catalog,
|
|
58
|
+
* groups triggers by their distinct `event.config.repo` (one API call per repo
|
|
59
|
+
* per tick regardless of how many triggers watch it), and diffs each repo's
|
|
60
|
+
* PR list against a persisted ETag + cursor to find genuinely new activity.
|
|
61
|
+
*
|
|
62
|
+
* The first tick for a never-before-seen repo only seeds the cursor — it
|
|
63
|
+
* never fires for a repo's pre-existing PR history, mirroring how
|
|
64
|
+
* `ScheduleSource` seeds `next_run_at` without firing on first sight.
|
|
65
|
+
*/
|
|
66
|
+
export declare class GithubPollSource implements TriggerSourcePort {
|
|
67
|
+
private readonly store;
|
|
68
|
+
private readonly cwd;
|
|
69
|
+
private readonly intervalMs;
|
|
70
|
+
private readonly logError;
|
|
71
|
+
private readonly env;
|
|
72
|
+
private readonly createGithubClient;
|
|
73
|
+
private interval;
|
|
74
|
+
private inFlight;
|
|
75
|
+
constructor(options: GithubPollSourceOptions);
|
|
76
|
+
start(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
|
|
77
|
+
stop(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* One discovery + poll + fire pass. Public so tests can drive it directly
|
|
80
|
+
* instead of waiting on real interval timers.
|
|
81
|
+
*/
|
|
82
|
+
tick(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
|
|
83
|
+
private discover;
|
|
84
|
+
private pollRepo;
|
|
85
|
+
private fireSafely;
|
|
86
|
+
}
|