ai-spend-agent 0.9.0 → 0.9.2

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.
@@ -1,4 +1,4 @@
1
- import { type AcceptedOutcomeV0, type AcceptedOutcomeV0DraftInput, type ApprovalEventV0, type ApprovalEventV0DraftInput, type ConfirmedOwnershipV0 } from "@agent-finops/core";
1
+ import { isCredentialLike, isPathLike, type AcceptedOutcomeV0, type AcceptedOutcomeV0DraftInput, type ApprovalEventV0, type ApprovalEventV0DraftInput, type ConfirmedOwnershipV0 } from "@agent-finops/core";
2
2
  export declare const PROJECT_ACCOUNTABILITY_STATE_FILE = "project-accountability.json";
3
3
  export declare const PROJECT_ACCOUNTABILITY_STATE_KIND: "aibill.project_accountability_store";
4
4
  export declare const PROJECT_ACCOUNTABILITY_STATE_VERSION: 1;
@@ -85,11 +85,10 @@ export declare function appendProjectApprovalEvent(rootPath: string, input: Omit
85
85
  */
86
86
  export declare function appendAcceptedProjectOutcome(rootPath: string, value: AcceptedOutcomeV0 | AcceptedOutcomeV0DraftInput): Promise<ProjectAccountabilityStateV1>;
87
87
  /**
88
- * Exported as the classification FLOOR for the guided-prompt engine: the
89
- * per-prompt classifier must reject at least everything these predicates
90
- * reject, so `parseDisplayLabel` can never abort on an answer the prompt
91
- * already accepted (the Aug 17 incident class).
88
+ * The classification FLOOR predicates for the guided-prompt engine live in
89
+ * core (`guidedAnswer.ts`) so the CLI classifier, the MCP draft preview,
90
+ * and this accountability backstop can never drift apart. Re-exported here
91
+ * for existing CLI imports.
92
92
  */
93
- export declare function isPathLike(value: string): boolean;
94
- export declare function isCredentialLike(value: string): boolean;
93
+ export { isCredentialLike, isPathLike };
95
94
  //# sourceMappingURL=projectAccountabilityState.d.ts.map
@@ -5,7 +5,7 @@ import { lstat, mkdir, open, readdir, realpath, rmdir, unlink } from "node:fs/pr
5
5
  import { homedir } from "node:os";
6
6
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { promisify } from "node:util";
8
- import { CONFIRMED_OWNERSHIP_V0_KIND, MAX_APPROVAL_EVENTS_V0, PROJECT_ECONOMICS_V0_VERSION, appendApprovalEventV0, createAcceptedOutcomeV0, createConfirmedOwnershipV0, createProjectEconomicsReference, parseAcceptedOutcomeV0, parseApprovalEventV0, parseConfirmedOwnershipV0, readSafeStateText, resolveSafeScanRoot, UnsafeStateFileError, writeSafeStateText } from "@agent-finops/core";
8
+ import { CONFIRMED_OWNERSHIP_V0_KIND, isCredentialLike, isPathLike, MAX_APPROVAL_EVENTS_V0, PROJECT_ECONOMICS_V0_VERSION, appendApprovalEventV0, createAcceptedOutcomeV0, createConfirmedOwnershipV0, createProjectEconomicsReference, parseAcceptedOutcomeV0, parseApprovalEventV0, parseConfirmedOwnershipV0, readSafeStateText, resolveSafeScanRoot, UnsafeStateFileError, writeSafeStateText } from "@agent-finops/core";
9
9
  export const PROJECT_ACCOUNTABILITY_STATE_FILE = "project-accountability.json";
10
10
  export const PROJECT_ACCOUNTABILITY_STATE_KIND = "aibill.project_accountability_store";
11
11
  export const PROJECT_ACCOUNTABILITY_STATE_VERSION = 1;
@@ -794,21 +794,12 @@ function noFollowFlag() {
794
794
  return typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
795
795
  }
796
796
  /**
797
- * Exported as the classification FLOOR for the guided-prompt engine: the
798
- * per-prompt classifier must reject at least everything these predicates
799
- * reject, so `parseDisplayLabel` can never abort on an answer the prompt
800
- * already accepted (the Aug 17 incident class).
797
+ * The classification FLOOR predicates for the guided-prompt engine live in
798
+ * core (`guidedAnswer.ts`) so the CLI classifier, the MCP draft preview,
799
+ * and this accountability backstop can never drift apart. Re-exported here
800
+ * for existing CLI imports.
801
801
  */
802
- export function isPathLike(value) {
803
- return /^(?:~?[\\/]|\.{1,2}(?:[\\/]|$)|[A-Za-z]:[\\/]|\\\\)/u.test(value) ||
804
- /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(value) || value.includes("\\");
805
- }
806
- export function isCredentialLike(value) {
807
- return /(?:sk-(?:ant-|proj-)?|sk_|gh[pousr]_|github_pat_|npm_|AIza|xox[baprs]-|glpat-|AKIA)[A-Za-z0-9_-]{8,}/i
808
- .test(value) ||
809
- /(?:api[_ -]?key|access[_ -]?token|auth[_ -]?token|secret|password)\s*[:=]\s*\S+/i
810
- .test(value);
811
- }
802
+ export { isCredentialLike, isPathLike };
812
803
  function hasUnpairedSurrogate(value) {
813
804
  for (let index = 0; index < value.length; index += 1) {
814
805
  const code = value.charCodeAt(index);
@@ -0,0 +1,272 @@
1
+ /**
2
+ * CLI email capture — the launch-list signup lane (v0.9.2).
3
+ *
4
+ * Design: docs/qa-handoff/CLI_CAPTURE_DESIGN.md (see its dated placement
5
+ * addendum). QA verdict (its B/M fixes are mandatory):
6
+ * docs/qa-handoff/CLI_CAPTURE_QA_VERDICT.md.
7
+ *
8
+ * Placement (founder decision 2026-08-24): the ONE ask runs PRE-RECEIPT,
9
+ * DURING the first evidence scan — it fills the first-run wait instead of
10
+ * adding one. The receipt renders when both the human's answer (or skip /
11
+ * timeout / Ctrl-C) and the pipeline resolve; consent, when an email was
12
+ * typed, renders strictly AFTER the receipt.
13
+ *
14
+ * Hard rules encoded here:
15
+ * - The payload is EXACTLY {email, ref} — the deployed asktilden.com route
16
+ * reads `ref` (not `sourceRef`) and normalizes anything failing
17
+ * /^[a-z0-9][a-z0-9_-]{0,63}$/ (dots included) to "direct" (verdict B1).
18
+ * - Consent shows the literal payload JSON; the sent line claims only the
19
+ * payload layer, never "nothing else left this machine" (verdict B2).
20
+ * - One POST attempt, 3s timeout, no retry, no queue, and the typed email is
21
+ * never persisted on failure.
22
+ * - Every read is timeout-guarded and can never eat a skip: a timeout or
23
+ * Ctrl-C records NO lifetime skip (verdict M3).
24
+ * - Skip = two empty Enters with one declarative nudge between them; the
25
+ * completed pair counts as ONE lifetime skip. This is the founder-approved
26
+ * ceiling: no further skip friction may be added, and removal of skip is
27
+ * refused by design.
28
+ * - Never-ask state lives in ~/.aibill/signup.json (the existing home dir —
29
+ * verdict M5) and fails CLOSED to never-ask on corrupt/readonly state.
30
+ */
31
+ export declare const waitlistUrl = "https://asktilden.com/api/waitlist";
32
+ /** Closed payload type — adding a field here must fail the creep-guard test. */
33
+ export type WaitlistPayload = {
34
+ email: string;
35
+ ref: string;
36
+ };
37
+ export type WaitlistRefSurface = "receipt" | "signup";
38
+ export declare const deployedRefPattern: RegExp;
39
+ export declare function normalizeWaitlistEmail(raw: string): string | undefined;
40
+ /** `--ref` allowlist: lowercase alphanumerics and dashes, 1–24 chars. */
41
+ export declare function sanitizeSignupRefTag(raw: string): string | undefined;
42
+ export declare function buildWaitlistRef(surface: WaitlistRefSurface, tag?: string): string;
43
+ /**
44
+ * The exact bytes sent — key order pinned. Payload creep (adding os/plan/
45
+ * version data, or stuffing values into ref) fails the CI creep-guard test;
46
+ * see docs/qa-handoff/CLI_CAPTURE_DESIGN.md §3c before touching this.
47
+ */
48
+ export declare function serializeWaitlistPayload(payload: WaitlistPayload): string;
49
+ export type WaitlistPostOutcome = "sent" | "invalid_email" | "rate_limited" | "unreachable";
50
+ /**
51
+ * Single POST, 3s timeout, one attempt, no retry, no queue. The response
52
+ * body is never printed (it could be an HTML error page); only the status
53
+ * code is mapped. Headers are the full explicit set this request adds —
54
+ * pinned by test so header creep fails CI too.
55
+ */
56
+ export declare function postWaitlistSignup(payload: WaitlistPayload, options?: {
57
+ fetchImpl?: typeof fetch;
58
+ timeoutMs?: number;
59
+ }): Promise<WaitlistPostOutcome>;
60
+ export type EmailDeliverability = "ok" | "no_mx" | "disposable";
61
+ /** Small static blocklist of throwaway-inbox domains (exact match only). */
62
+ export declare const disposableEmailDomains: ReadonlySet<string>;
63
+ /** Exact or subdomain match against the blocklist (QA m5). */
64
+ export declare function isDisposableEmailDomain(domain: string): boolean;
65
+ export type SignupDnsResolver = {
66
+ resolveMx: (domain: string) => Promise<Array<{
67
+ exchange: string;
68
+ priority: number;
69
+ }>>;
70
+ resolve4: (domain: string) => Promise<string[]>;
71
+ };
72
+ export declare function assessEmailDeliverability(email: string, options?: {
73
+ resolver?: SignupDnsResolver;
74
+ timeoutMs?: number;
75
+ }): Promise<EmailDeliverability>;
76
+ export declare const signupCopy: {
77
+ /**
78
+ * Launch-week headline — TRUE only through launch day (Fri Aug 28); the
79
+ * date gate in signupAskBlockLines swaps in the evergreen fallback after
80
+ * signupLaunchWindowEndsAt so this copy can never go stale.
81
+ */
82
+ readonly askHeadlineLaunch: readonly ["aibill launches Friday with Star.fun.", "Get the launch email + what ships next:"];
83
+ readonly askHeadlineFallback: readonly ["Get product updates:"];
84
+ readonly askFooter: "type your email, or press Enter to skip";
85
+ readonly askPrompt: " > ";
86
+ readonly waitLine: "reading your local AI evidence…";
87
+ /** Printed once if the scan finishes while the ask is still open. */
88
+ readonly receiptReadyLine: "your receipt is ready — type your email, or press Enter to see it";
89
+ /** Printed after the FIRST empty Enter; the second empty Enter skips. */
90
+ readonly skipNudgeLine: "one launch email · Enter again to skip, or type your email";
91
+ readonly scopeLine: "used only for updates · never shared";
92
+ /** Printed on the first Ctrl-C at the ask (the scan continues; M2). */
93
+ readonly interruptLine: "ok — skipped the ask · your receipt is still being read (Ctrl-C again to quit)";
94
+ /** Printed when ^C lands after the ask settled but before the receipt. */
95
+ readonly interruptAfterAnswerLine: "still reading your evidence · Ctrl-C again to quit";
96
+ /** Printed when the re-prompt budget closes the ask (no skip consumed). */
97
+ readonly budgetClosedLine: "moving on · npx aibill signup <email> anytime";
98
+ readonly consentQuestion: (payloadJson: string) => string;
99
+ readonly sentLine: "sent: exactly that JSON · nothing else in the payload";
100
+ readonly neverLine: "ok — never asking again · npx aibill signup <email> if you change your mind";
101
+ readonly invalidEmailLine: "that email did not validate — check it and retry";
102
+ readonly noMxLine: "that domain can't receive email — check the spelling";
103
+ readonly disposableLine: "use an address you actually check";
104
+ readonly rateLimitedLine: "busy — try again in a minute, or join at https://asktilden.com";
105
+ readonly unreachableLine: "could not reach asktilden.com — join at https://asktilden.com";
106
+ readonly alreadyLine: "already on the list · nothing sent twice";
107
+ readonly nothingSentLine: "nothing sent";
108
+ readonly nonInteractiveLine: "signup needs an interactive terminal — or join at https://asktilden.com";
109
+ readonly forgetLine: "local signup state cleared · to leave the list, reply to any email or write hello@asktilden.com";
110
+ readonly receiptPointer: "npx aibill signup <email> launch updates · optional · email only";
111
+ readonly samplePointer: "launch updates: npx aibill signup <email> · optional · email only";
112
+ readonly initPointer: "launch updates (optional): npx aibill signup <email>";
113
+ readonly statuslinePointer: "launch updates: npx aibill signup <email> · optional";
114
+ };
115
+ /**
116
+ * End of the launch-week headline window (the Friday line is true only
117
+ * through launch day, Fri 2026-08-28). After this instant the ask block
118
+ * automatically uses the evergreen fallback headline.
119
+ */
120
+ export declare const signupLaunchWindowEndsAt = "2026-08-29T00:00:00Z";
121
+ /**
122
+ * The standout ask block: full-width rule (the receipt's own rule
123
+ * convention), then the headline for the current window, then the skip
124
+ * footer. Text lines carry the receipt's two-space indent.
125
+ */
126
+ export declare function signupAskBlockLines(now: Date, width: number): string[];
127
+ /** Same clamp the receipt renderer applies (COLUMNS → tty → 72, 40..120). */
128
+ export declare function signupTerminalWidth(): number;
129
+ export type SignupStatus = "subscribed" | "never" | "deferred";
130
+ export type SignupState = {
131
+ version: 1;
132
+ status: SignupStatus;
133
+ /** Lifetime count of completed explicit skips (double-Enter / declined consent). */
134
+ askCount: number;
135
+ /**
136
+ * Lifetime count of ask OPENINGS (stamped even when the human never
137
+ * answers). Timeouts consume no skip, so this cap is what ends the asks
138
+ * for perpetual walk-away users (QA m10).
139
+ */
140
+ stampCount?: number;
141
+ lastAskedAt?: string;
142
+ /** Stored only after a successful subscribe, only locally. */
143
+ email?: string;
144
+ };
145
+ export type SignupStateRead = {
146
+ kind: "fresh";
147
+ } | {
148
+ kind: "ok";
149
+ state: SignupState;
150
+ } | {
151
+ kind: "unreadable";
152
+ };
153
+ export declare function signupStateFilePath(homeDirectory?: string): string;
154
+ export declare function readSignupState(filePath: string): Promise<SignupStateRead>;
155
+ /** Atomic-ish write; returns false instead of throwing so callers fail closed. */
156
+ export declare function writeSignupState(filePath: string, state: SignupState): Promise<boolean>;
157
+ export declare function clearSignupState(filePath: string): Promise<boolean>;
158
+ /** Hard lifetime ceiling on ask openings, decided answer or not (QA m10). */
159
+ export declare const maximumLifetimeAskStamps = 6;
160
+ /**
161
+ * Whether the ONE ask may run. Fails closed on unreadable state; a
162
+ * future-dated lastAskedAt (clock moved back) counts as recently asked,
163
+ * never as a reason to re-ask.
164
+ */
165
+ export declare function signupAskAllowed(read: SignupStateRead, now: Date): boolean;
166
+ export type SignupAskIo = {
167
+ /** Resolves the typed line, or undefined when the read timed out / aborted. */
168
+ question: (query: string, timeoutMs: number) => Promise<string | undefined>;
169
+ write: (line: string) => void;
170
+ /** Raw prompt redraw after a nudge line (no newline appended). */
171
+ writeRaw?: (text: string) => void;
172
+ };
173
+ export declare const signupAskTimeoutMs = 30000;
174
+ export type PreReceiptAskOutcome = {
175
+ kind: "no_ask";
176
+ } | {
177
+ kind: "skipped";
178
+ } | {
179
+ kind: "email";
180
+ payload: WaitlistPayload;
181
+ stamped: SignupState;
182
+ };
183
+ export type PreReceiptAskSession = {
184
+ /** Resolves when the human answered, skipped, timed out, or interrupted. */
185
+ outcome: Promise<PreReceiptAskOutcome>;
186
+ /**
187
+ * Ready-nudge: the pipeline finished while the ask is still open. Prints
188
+ * one line and re-draws the prompt; the open read's timer is NOT reset.
189
+ */
190
+ notifyReceiptReady: () => void;
191
+ };
192
+ /**
193
+ * Opens the ONE ask, or returns undefined (with zero output) when state
194
+ * disallows it — subsequent runs stay byte-identical to the fast path.
195
+ *
196
+ * Decision ledger (unchanged semantics, earlier placement):
197
+ * - timeout / Ctrl-C → no decision: lastAskedAt stamped, NO skip
198
+ * - Enter, then Enter again → ONE lifetime skip (nudge line between)
199
+ * - n / never → never ask again
200
+ * - typed input → email path (format + deliverability checks
201
+ * re-prompt without consuming skips)
202
+ * The consent step (literal payload JSON → y/N → single POST) runs strictly
203
+ * AFTER the receipt via runSignupConsentAfterReceipt.
204
+ */
205
+ export declare function openPreReceiptSignupAsk(options: {
206
+ io: SignupAskIo;
207
+ stateFilePath: string;
208
+ now?: () => Date;
209
+ width?: number;
210
+ /** Lines printed before the ask block (the terminal wait line). */
211
+ preambleLines?: readonly string[];
212
+ dnsResolver?: SignupDnsResolver;
213
+ }): Promise<PreReceiptAskSession | undefined>;
214
+ /**
215
+ * The consent step, strictly AFTER the receipt has printed: scope line, the
216
+ * literal payload JSON, a typed y — then ONE POST. Failures never persist,
217
+ * retry, or queue the typed email.
218
+ */
219
+ export declare function runSignupConsentAfterReceipt(options: {
220
+ io: SignupAskIo;
221
+ stateFilePath: string;
222
+ payload: WaitlistPayload;
223
+ stamped: SignupState;
224
+ fetchImpl?: typeof fetch;
225
+ }): Promise<void>;
226
+ /**
227
+ * Which argv shapes qualify for the during-scan ask: the real receipt path
228
+ * only — the default/quickstart command (flag-form included) — and never
229
+ * sample, drill-downs, JSON, help/version, or any named command (improve,
230
+ * report, init, …).
231
+ */
232
+ export declare function qualifiesForPreReceiptSignupAsk(argv: readonly string[]): boolean;
233
+ /**
234
+ * Concurrency shell: run the evidence pipeline and the open ask together.
235
+ * The receipt renders only after BOTH resolve; a pipeline error is captured
236
+ * (never an unhandled rejection) and still waits for the bounded ask, so a
237
+ * prompt can never hang over a dead pipeline longer than its own timeout.
238
+ * If the pipeline settles first, the session gets the one ready-nudge.
239
+ */
240
+ export declare function orchestratePreReceiptAsk<T>(input: {
241
+ session: PreReceiptAskSession | undefined;
242
+ runPipeline: () => Promise<T>;
243
+ }): Promise<{
244
+ pipeline: {
245
+ ok: true;
246
+ value: T;
247
+ } | {
248
+ ok: false;
249
+ error: unknown;
250
+ };
251
+ outcome: PreReceiptAskOutcome;
252
+ }>;
253
+ export type TerminalPreReceiptAsk = {
254
+ session: PreReceiptAskSession;
255
+ /** True when the last read ended without the user pressing Enter. */
256
+ needsFreshLine: () => boolean;
257
+ runConsent: (outcome: Extract<PreReceiptAskOutcome, {
258
+ kind: "email";
259
+ }>) => Promise<void>;
260
+ close: () => void;
261
+ };
262
+ /**
263
+ * Opens the during-scan ask on the real terminal: its own readline with
264
+ * per-read AbortSignal timeouts. The timeout race can never eat a typed
265
+ * skip — either the line resolved first (the skip is recorded) or the abort
266
+ * won and NOTHING is recorded; the wrong decision is unrepresentable.
267
+ * Ctrl-C cancels only the ask (no skip consumed, state already stamped);
268
+ * the receipt still renders. Returns undefined — with zero output — when
269
+ * signup state disallows the ask.
270
+ */
271
+ export declare function openPreReceiptSignupAskInTerminal(): Promise<TerminalPreReceiptAsk | undefined>;
272
+ //# sourceMappingURL=signup.d.ts.map