@hraness/ghostget 0.18.10 → 0.18.13

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/src/ghostget.ts CHANGED
@@ -839,6 +839,7 @@ async function runCaptureCommand(
839
839
  output: Output,
840
840
  dependencies: GhostgetDependencies,
841
841
  signal?: AbortSignal,
842
+ onUsefulResult?: () => void,
842
843
  ): Promise<number> {
843
844
  const resolved = resolveCaptureArgumentsWithAuth(arguments_, environment);
844
845
  const unresolvedArguments = [...prefix, ...resolved.arguments];
@@ -866,16 +867,18 @@ async function runCaptureCommand(
866
867
  if (persistentPrivateCapture) {
867
868
  ensurePrivateStateDirectory(gmailOptions.outputBase, environment);
868
869
  }
869
- return dependencies.gmailCaptureMain(
870
+ const code = await dependencies.gmailCaptureMain(
870
871
  gmailOptions,
871
872
  resolved.auth,
872
873
  environment,
873
874
  output,
874
875
  );
876
+ if (code === 0) onUsefulResult?.();
877
+ return code;
875
878
  }
876
879
  const prepared = prepareCapture(resolved, environment);
877
880
  try {
878
- return await dependencies.clipMain(
881
+ const code = await dependencies.clipMain(
879
882
  [...prefix, ...prepared.arguments],
880
883
  environment,
881
884
  output,
@@ -888,6 +891,8 @@ async function runCaptureCommand(
888
891
  },
889
892
  prepared.runtimeOptions,
890
893
  );
894
+ if (code === 0 && (parsed.value.command === "capture" || parsed.value.command === "inspect")) onUsefulResult?.();
895
+ return code;
891
896
  } finally {
892
897
  prepared.cleanup();
893
898
  }
@@ -1972,6 +1977,7 @@ async function runCommand(
1972
1977
  output: Output,
1973
1978
  dependencies: GhostgetDependencies,
1974
1979
  signal?: AbortSignal,
1980
+ onUsefulResult?: () => void,
1975
1981
  ): Promise<number> {
1976
1982
  if (arguments_.command === "help") {
1977
1983
  output.stdout(renderGhostgetUsage());
@@ -2031,6 +2037,7 @@ async function runCommand(
2031
2037
  output,
2032
2038
  dependencies,
2033
2039
  signal,
2040
+ onUsefulResult,
2034
2041
  );
2035
2042
  }
2036
2043
  if (arguments_.command === "read") {
@@ -2041,12 +2048,14 @@ async function runCommand(
2041
2048
  output,
2042
2049
  dependencies,
2043
2050
  signal,
2051
+ onUsefulResult,
2044
2052
  );
2045
2053
  }
2046
2054
  if (arguments_.command === "media") {
2047
2055
  const media = await dependencies.loadMediaRuntime();
2048
2056
  return media.runCli(arguments_.arguments, {
2049
2057
  io: output,
2058
+ ...(onUsefulResult === undefined ? {} : { onUsefulResult }),
2050
2059
  environment,
2051
2060
  ...(signal === undefined ? {} : { signal }),
2052
2061
  });
@@ -2066,6 +2075,7 @@ async function runCommand(
2066
2075
  output.stdout(
2067
2076
  runtime.encodeApplePhotosContactEvidenceCliOutput(result, arguments_.json),
2068
2077
  );
2078
+ onUsefulResult?.();
2069
2079
  return 0;
2070
2080
  }
2071
2081
  if (arguments_.command === "whatsapp-export-message-like-me") {
@@ -2097,6 +2107,7 @@ async function runCommand(
2097
2107
  output.stdout(`Manifest SHA-256: ${safe(result.bundle.manifestSha256)}\n`);
2098
2108
  output.stdout("Remote history completeness is not claimed. No cloud sync occurred.\n");
2099
2109
  }
2110
+ onUsefulResult?.();
2100
2111
  return 0;
2101
2112
  } finally {
2102
2113
  admission.release();
@@ -2136,6 +2147,7 @@ async function runCommand(
2136
2147
  ...(signal === undefined ? {} : { signal }),
2137
2148
  });
2138
2149
  output.stdout(runtime.encodeBeeperContactInteractionExportResult(result));
2150
+ onUsefulResult?.();
2139
2151
  return 0;
2140
2152
  } finally {
2141
2153
  admission.release();
@@ -2184,6 +2196,7 @@ async function runCommand(
2184
2196
  counts: result.manifest.counts,
2185
2197
  });
2186
2198
  print(output, summary, arguments_.json);
2199
+ onUsefulResult?.();
2187
2200
  return 0;
2188
2201
  } finally {
2189
2202
  admission.release();
@@ -2218,6 +2231,7 @@ async function runCommand(
2218
2231
  environment,
2219
2232
  );
2220
2233
  output.stdout(exactTerminalJson(receipt));
2234
+ if (arguments_.command === "messaging-context") onUsefulResult?.();
2221
2235
  return 0;
2222
2236
  }
2223
2237
  if (arguments_.command === "doctor") {
@@ -2259,7 +2273,11 @@ async function runCommand(
2259
2273
  }
2260
2274
  return 0;
2261
2275
  }
2262
- if (arguments_.command === "thread-split") return runThreadSplit(arguments_, output);
2276
+ if (arguments_.command === "thread-split") {
2277
+ const code = await runThreadSplit(arguments_, output);
2278
+ if (code === 0) onUsefulResult?.();
2279
+ return code;
2280
+ }
2263
2281
  if (arguments_.command === "thread-publish") {
2264
2282
  return runThreadPublish(
2265
2283
  arguments_,
@@ -3156,6 +3174,7 @@ async function runCommand(
3156
3174
  });
3157
3175
  if (arguments_.json) output.stdout(exactTerminalJson(value));
3158
3176
  else print(output, value, false);
3177
+ if (value.view?.sources.some((source) => source.coverage.state === "observed")) onUsefulResult?.();
3159
3178
  return 0;
3160
3179
  }
3161
3180
  if (arguments_.command === "invoke") {
@@ -3242,6 +3261,7 @@ async function runCommand(
3242
3261
  const view = cachedInvocationView(result);
3243
3262
  if (arguments_.json) output.stdout(exactTerminalJson(view));
3244
3263
  else print(output, view, false);
3264
+ if (result.status === "hit") onUsefulResult?.();
3245
3265
  return result.status === "hit" ? 0 : 3;
3246
3266
  }
3247
3267
  const result = await dependencies.revalidatePreparedCapability(
@@ -3256,6 +3276,7 @@ async function runCommand(
3256
3276
  const view = revalidatedInvocationView(result);
3257
3277
  if (arguments_.json) output.stdout(exactTerminalJson(view));
3258
3278
  else print(output, view, false);
3279
+ if (result.live.receipt.status === "succeeded" || result.live.receipt.status === "submitted") onUsefulResult?.();
3259
3280
  return result.live.receipt.status === "succeeded" || result.live.receipt.status === "submitted" ? 0 : result.live.receipt.status === "indeterminate" ? 5 : 3;
3260
3281
  }
3261
3282
  const stored = createAndSaveInvocationPlan(
@@ -3325,6 +3346,7 @@ async function runCommand(
3325
3346
  result.receiptBinding,
3326
3347
  environment,
3327
3348
  );
3349
+ if (result.receipt.state === "submitted") onUsefulResult?.();
3328
3350
  return result.receipt.state === "submitted"
3329
3351
  ? 0
3330
3352
  : result.receipt.state === "indeterminate"
@@ -3342,6 +3364,7 @@ async function runCommand(
3342
3364
  ...(signal === undefined ? {} : { signal }),
3343
3365
  });
3344
3366
  print(output, invocationView(result), arguments_.json);
3367
+ if (result.receipt.status === "succeeded" || result.receipt.status === "submitted") onUsefulResult?.();
3345
3368
  return result.receipt.status === "succeeded" || result.receipt.status === "submitted" ? 0 : result.receipt.status === "indeterminate" ? 5 : 3;
3346
3369
  }
3347
3370
  if (arguments_.command === "runs-list") {
@@ -3483,6 +3506,7 @@ export async function main(
3483
3506
  output: Output = defaultOutput,
3484
3507
  dependencyOverrides: Partial<GhostgetDependencies> = {},
3485
3508
  signal?: AbortSignal,
3509
+ onUsefulResult?: () => void,
3486
3510
  ): Promise<number> {
3487
3511
  const parsed = parseGhostgetArguments(rawArguments);
3488
3512
  if (!parsed.ok) {
@@ -3508,13 +3532,19 @@ export async function main(
3508
3532
  ).registry,
3509
3533
  };
3510
3534
  }
3511
- return await runCommand(
3535
+ let usefulResult = false;
3536
+ const code = await runCommand(
3512
3537
  parsed.value,
3513
3538
  environment,
3514
3539
  output,
3515
3540
  dependencies,
3516
3541
  signal,
3542
+ () => { usefulResult = true; },
3517
3543
  );
3544
+ if (code === 0 && usefulResult && signal?.aborted !== true) {
3545
+ try { void Promise.resolve(onUsefulResult?.()).catch(() => undefined); } catch { /* Optional observation cannot alter completed work. */ }
3546
+ }
3547
+ return code;
3518
3548
  } catch (error) {
3519
3549
  if (error instanceof LinkedDeviceLifecycleIndeterminateError) {
3520
3550
  const failure = {
@@ -3600,6 +3630,7 @@ type GhostgetProcessBoundaryDependencies = {
3600
3630
  ) => () => void;
3601
3631
  readonly resendSignal: (signal: NodeJS.Signals) => void;
3602
3632
  readonly setExitCode: (code: number) => void;
3633
+ readonly onUsefulResult?: () => void;
3603
3634
  };
3604
3635
 
3605
3636
  function subscribeProcessTermination(
@@ -3631,6 +3662,7 @@ export async function runGhostgetProcess(
3631
3662
  overrides: Partial<GhostgetProcessBoundaryDependencies> = {},
3632
3663
  ): Promise<void> {
3633
3664
  const dependencies: GhostgetProcessBoundaryDependencies = {
3665
+ ...(overrides.onUsefulResult === undefined ? {} : { onUsefulResult: overrides.onUsefulResult }),
3634
3666
  rawArguments: overrides.rawArguments ?? process.argv.slice(2),
3635
3667
  environment: overrides.environment ?? process.env,
3636
3668
  output: overrides.output ?? defaultOutput,
@@ -3667,18 +3699,25 @@ export async function runGhostgetProcess(
3667
3699
  );
3668
3700
  };
3669
3701
  unsubscribeTermination = dependencies.subscribeTermination(terminate);
3702
+ let usefulResult = false;
3703
+ let code: number | undefined;
3670
3704
  try {
3671
- dependencies.setExitCode(await dependencies.runMain(
3705
+ code = await dependencies.runMain(
3672
3706
  dependencies.rawArguments,
3673
3707
  dependencies.environment,
3674
3708
  dependencies.output,
3675
3709
  {},
3676
3710
  controller.signal,
3677
- ));
3711
+ () => { usefulResult = true; },
3712
+ );
3713
+ dependencies.setExitCode(code);
3678
3714
  } finally {
3679
3715
  terminationState.cancelFallback?.();
3680
3716
  unsubscribeTermination();
3681
3717
  }
3718
+ if (code === 0 && usefulResult && !controller.signal.aborted) {
3719
+ try { void Promise.resolve(dependencies.onUsefulResult?.()).catch(() => undefined); } catch { /* Keep the completed command outcome. */ }
3720
+ }
3682
3721
  }
3683
3722
 
3684
3723
  if (import.meta.main) {
package/src/media/cli.ts CHANGED
@@ -38,6 +38,7 @@ export interface RunCliOptions {
38
38
  readonly homeDirectory?: string;
39
39
  readonly signal?: AbortSignal;
40
40
  readonly dependencies?: MediaCliDependencies;
41
+ readonly onUsefulResult?: () => void;
41
42
  }
42
43
 
43
44
  const processIo: CliIo = {
@@ -163,6 +164,10 @@ export async function runCli(argv: readonly string[], options: RunCliOptions = {
163
164
  const environment = options.environment ?? process.env;
164
165
  const homeDirectory = options.homeDirectory ?? homedir();
165
166
  const dependencies = options.dependencies ?? defaultDependencies;
167
+ const completed = (): void => {
168
+ if (options.signal?.aborted === true) return;
169
+ try { void Promise.resolve(options.onUsefulResult?.()).catch(() => undefined); } catch { /* Observation cannot change an archive result. */ }
170
+ };
166
171
  const parsed = parseArgs(argv);
167
172
  if (!parsed.ok) {
168
173
  if (parsed.json) writeJson(io, "stderr", { ok: false, error: { code: "USAGE", message: parsed.message } });
@@ -191,6 +196,7 @@ export async function runCli(argv: readonly string[], options: RunCliOptions = {
191
196
  if (command.json) writeJson(io, result.ok ? "stdout" : "stderr", { ok: result.ok, verification: result });
192
197
  else if (result.ok) io.stdout(`Verified ${sanitizeTerminalText(result.assetKey ?? command.itemDirectory)}: ${String(result.checkedArtifacts)} artifacts\n`);
193
198
  else io.stderr(`ghostget media: verification failed\n${result.failures.map((failure) => `- ${redactDiagnostic(failure, { homeDirectory })}`).join("\n")}\n`);
199
+ if (result.ok) completed();
194
200
  return result.ok ? 0 : 8;
195
201
  }
196
202
  if (command.kind === "transcriber-setup") {
@@ -258,6 +264,7 @@ export async function runCli(argv: readonly string[], options: RunCliOptions = {
258
264
  });
259
265
  if (command.json) writeJson(io, "stdout", captureSummary(result));
260
266
  else io.stdout(humanCaptureSummary(result));
267
+ completed();
261
268
  return 0;
262
269
  } catch (error) {
263
270
  const secrets = [
package/src/messaging.ts CHANGED
@@ -75,6 +75,8 @@ function environmentSnapshot(
75
75
  else result[key] = value;
76
76
  }
77
77
  }
78
+ // Package-owned SDK children never emit standalone support notices.
79
+ result.GHOSTGET_CLI_DEPTH = "1";
78
80
  return Object.freeze(result);
79
81
  }
80
82
 
@@ -526,6 +526,8 @@ function snapshotEnvironment(value: unknown): Readonly<Record<string, string>> {
526
526
  defineEnvironmentValue(result, key, environmentValue);
527
527
  }
528
528
  }
529
+ // Package-owned SDK children never emit standalone support notices.
530
+ result.GHOSTGET_CLI_DEPTH = "1";
529
531
  if (value === undefined) return Object.freeze(result);
530
532
  const overrides = record(value, "Ghostget omni client environment");
531
533
  for (const [key, environmentValue] of Object.entries(overrides)) {
@@ -541,6 +543,7 @@ function snapshotEnvironment(value: unknown): Readonly<Record<string, string>> {
541
543
  defineEnvironmentValue(result, name, environmentValue);
542
544
  }
543
545
  }
546
+ result.GHOSTGET_CLI_DEPTH = "1";
544
547
  return Object.freeze(result);
545
548
  }
546
549
 
@@ -448,18 +448,38 @@ export async function runImsgRpc(
448
448
  let timedOut = false;
449
449
  let cancelled = false;
450
450
  let forceKill: ReturnType<typeof setTimeout> | null = null;
451
+ let groupTerminated: Promise<boolean> | null = null;
451
452
  let terminationStarted = false;
452
453
  const signalGroup = (signal: "SIGTERM" | "SIGKILL"): void => {
453
454
  try {
454
455
  process.kill(-child.pid, signal);
455
456
  } catch {
456
- // child.exited remains the reaping proof.
457
+ // The bounded group probe below establishes descendant cleanup.
457
458
  }
458
459
  };
459
460
  const terminate = (): void => {
460
461
  if (!terminationStarted) {
461
462
  terminationStarted = true;
462
463
  signalGroup("SIGTERM");
464
+ groupTerminated = (async (): Promise<boolean> => {
465
+ // A joined leader and closed pipes do not prove its descendants exited.
466
+ // Retain the one-second escalation, then bound final group reaping.
467
+ const deadline = performance.now() + 1_500;
468
+ for (;;) {
469
+ try {
470
+ process.kill(-child.pid, 0);
471
+ } catch (error) {
472
+ const code = typeof error === "object" && error !== null
473
+ && "code" in error ? error.code : undefined;
474
+ if (code === "ESRCH") return true;
475
+ // Allow a transient permission denial to settle within the same
476
+ // bound; it is never proof that the group exited.
477
+ if (code !== "EPERM") return false;
478
+ }
479
+ if (performance.now() >= deadline) return false;
480
+ await new Promise<void>((resolve) => setTimeout(resolve, 20));
481
+ }
482
+ })();
463
483
  }
464
484
  forceKill ??= setTimeout(() => signalGroup("SIGKILL"), 1_000);
465
485
  };
@@ -509,8 +529,14 @@ export async function runImsgRpc(
509
529
  });
510
530
  } finally {
511
531
  clearTimeout(timeout);
512
- if (forceKill !== null) clearTimeout(forceKill);
513
532
  invocation.signal?.removeEventListener("abort", onAbort);
533
+ try {
534
+ if (groupTerminated !== null && !await groupTerminated) {
535
+ throw new ImsgCleanupUnverifiedError();
536
+ }
537
+ } finally {
538
+ if (forceKill !== null) clearTimeout(forceKill);
539
+ }
514
540
  }
515
541
  }
516
542
 
@@ -0,0 +1,7 @@
1
+ /** Public product identity shared by the CLI, menu and static website. */
2
+ export const ghostgetSupportProfile = Object.freeze({
3
+ id: "wrench",
4
+ name: "Ghostget",
5
+ valueProposition: "Support ongoing development of precise web tools for agents.",
6
+ updates: true,
7
+ } as const);
package/src/support.ts CHANGED
@@ -2,13 +2,7 @@ import {
2
2
  maybeShowSupportInvitation,
3
3
  runSupportCommand,
4
4
  } from "@hraness/support-foundation/node";
5
-
6
- const profile = {
7
- id: "wrench",
8
- name: "Ghostget",
9
- valueProposition: "Support ongoing development of precise web tools for agents.",
10
- updates: true,
11
- } as const;
5
+ import { ghostgetSupportProfile as profile } from "./support-profile";
12
6
 
13
7
  type SupportOutput = {
14
8
  readonly stdout: (text: string) => unknown;
@@ -19,12 +13,12 @@ export async function runGhostgetSupportCommand(
19
13
  args: readonly string[],
20
14
  output: SupportOutput,
21
15
  ): Promise<number> {
22
- const result = await runSupportCommand(profile, args);
16
+ const result = await runSupportCommand(profile, args, { command: ["ghostget"] });
23
17
  if (result.stdout !== "") output.stdout(result.stdout);
24
18
  if (result.stderr !== "") output.stderr(result.stderr);
25
19
  return result.exitCode;
26
20
  }
27
21
 
28
22
  export async function showGhostgetSupportInvitation(): Promise<void> {
29
- await maybeShowSupportInvitation(profile, { usefulResult: true });
23
+ await maybeShowSupportInvitation(profile, { usefulResult: true, command: ["ghostget"] });
30
24
  }
package/src/usage.ts CHANGED
@@ -8,8 +8,10 @@
8
8
  export const ghostgetUsage = `Usage:
9
9
  ghostget --version Print the exact Ghostget release version
10
10
  ghostget support [--json] View optional product updates and support
11
- ghostget support offer --json Check whether an agent invitation is due
12
- ghostget support shown <id> Record an invitation presented to the user
11
+ ghostget support protocol --json Read the optional agent closeout protocol
12
+ ghostget support offer --json Claim an invitation when due at task closeout
13
+ ghostget support shown <id> Acknowledge human-facing output after presentation
14
+ ghostget support release <id> Cancel an invitation before presentation
13
15
  ghostget support dismiss|snooze|enable Set shared invitation preferences
14
16
  ghostget support status --json Inspect local invitation preferences
15
17
  ghostget init [directory] [--json] Initialize a Markdown vault
@@ -192,4 +194,16 @@ Omni views:
192
194
  and Notification entities. 'omni read --cache-only' returns the merged local view
193
195
  without provider work. A normal omni read explicitly revalidates each declared
194
196
  source; --from-exact-cache rebuilds derivatives from exact snapshots only.
197
+
198
+ Optional updates and support:
199
+ Successful standalone work may write a compact agent discovery notice to stderr,
200
+ including with --json or a PTY; command stdout and exit status stay unchanged.
201
+ Read support protocol --json, then check offer --json once at human task closeout.
202
+ Present persistently before shown <id>; acknowledgement records output, not consent.
203
+ Collapsed progress does not qualify. Final-only hosts omit shown; reservations expire
204
+ after ten minutes without a weekly cooldown, so a later task may offer again.
205
+ A quiet result needs no mention. No signup or payment occurs through these commands.
206
+ HRANESS_SUPPORT_AUDIENCE=agent|human|off selects presentation (default: agent).
207
+ Human presentation requires interactive stderr. Off suppresses due offers too.
208
+ HRANESS_SUPPORT=off disables ambient support; support dismiss opts out locally.
195
209
  `;
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Canonical immutable Ghostget package release identity. */
2
- export const GHOSTGET_VERSION = "0.18.10" as const;
2
+ export const GHOSTGET_VERSION = "0.18.13" as const;
@@ -321,6 +321,8 @@ function preparedEnvironment(
321
321
  for (const [key, value] of Object.entries(process.env)) {
322
322
  if (typeof value === "string") environment[key] = value;
323
323
  }
324
+ // Package-owned SDK children never emit standalone support notices.
325
+ environment.GHOSTGET_CLI_DEPTH = "1";
324
326
  if (additions === undefined) return Object.freeze(environment);
325
327
  const source = record(additions, "options.environment");
326
328
  for (const [key, value] of Object.entries(source)) {
@@ -332,6 +334,7 @@ function preparedEnvironment(
332
334
  return fail("environment value is malformed");
333
335
  } else environment[key] = value;
334
336
  }
337
+ environment.GHOSTGET_CLI_DEPTH = "1";
335
338
  return Object.freeze(environment);
336
339
  }
337
340