@cogcoin/client 1.0.1 → 1.1.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.
Files changed (79) hide show
  1. package/README.md +4 -2
  2. package/dist/bitcoind/client/factory.d.ts +0 -8
  3. package/dist/bitcoind/client/factory.js +1 -59
  4. package/dist/bitcoind/client/managed-client.d.ts +1 -3
  5. package/dist/bitcoind/client/managed-client.js +3 -47
  6. package/dist/bitcoind/indexer-daemon-main.js +173 -28
  7. package/dist/bitcoind/indexer-daemon.d.ts +14 -3
  8. package/dist/bitcoind/indexer-daemon.js +145 -29
  9. package/dist/bitcoind/indexer-monitor.d.ts +12 -0
  10. package/dist/bitcoind/indexer-monitor.js +89 -0
  11. package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
  12. package/dist/bitcoind/progress/follow-scene.js +87 -4
  13. package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
  14. package/dist/bitcoind/progress/tty-renderer.js +2 -0
  15. package/dist/bitcoind/retryable-rpc.js +3 -0
  16. package/dist/bitcoind/service.d.ts +1 -0
  17. package/dist/bitcoind/service.js +31 -9
  18. package/dist/bitcoind/testing.d.ts +0 -1
  19. package/dist/bitcoind/testing.js +0 -1
  20. package/dist/bitcoind/types.d.ts +5 -2
  21. package/dist/cli/commands/follow.js +44 -49
  22. package/dist/cli/commands/mining-admin.js +65 -2
  23. package/dist/cli/commands/mining-read.js +43 -3
  24. package/dist/cli/commands/mining-runtime.js +91 -73
  25. package/dist/cli/commands/service-runtime.js +42 -2
  26. package/dist/cli/commands/status.js +3 -1
  27. package/dist/cli/commands/sync.js +50 -90
  28. package/dist/cli/commands/update.d.ts +2 -0
  29. package/dist/cli/commands/update.js +101 -0
  30. package/dist/cli/commands/wallet-admin.js +21 -3
  31. package/dist/cli/commands/wallet-read.js +2 -0
  32. package/dist/cli/context.js +36 -1
  33. package/dist/cli/managed-indexer-observer.d.ts +33 -0
  34. package/dist/cli/managed-indexer-observer.js +163 -0
  35. package/dist/cli/mining-format.d.ts +3 -1
  36. package/dist/cli/mining-format.js +63 -0
  37. package/dist/cli/mining-json.d.ts +11 -1
  38. package/dist/cli/mining-json.js +15 -0
  39. package/dist/cli/output.js +74 -2
  40. package/dist/cli/parse.d.ts +1 -1
  41. package/dist/cli/parse.js +28 -0
  42. package/dist/cli/prompt.js +109 -0
  43. package/dist/cli/read-json.d.ts +26 -1
  44. package/dist/cli/read-json.js +48 -0
  45. package/dist/cli/runner.js +8 -2
  46. package/dist/cli/signals.d.ts +12 -0
  47. package/dist/cli/signals.js +31 -13
  48. package/dist/cli/types.d.ts +13 -4
  49. package/dist/cli/update-notifier.js +7 -222
  50. package/dist/cli/update-service.d.ts +34 -0
  51. package/dist/cli/update-service.js +152 -0
  52. package/dist/client/initialization.js +5 -0
  53. package/dist/semver.d.ts +12 -0
  54. package/dist/semver.js +68 -0
  55. package/dist/wallet/lifecycle.d.ts +10 -0
  56. package/dist/wallet/mining/config.js +64 -3
  57. package/dist/wallet/mining/control.d.ts +5 -1
  58. package/dist/wallet/mining/control.js +269 -26
  59. package/dist/wallet/mining/domain-prompts.d.ts +17 -0
  60. package/dist/wallet/mining/domain-prompts.js +130 -0
  61. package/dist/wallet/mining/index.d.ts +2 -1
  62. package/dist/wallet/mining/index.js +1 -0
  63. package/dist/wallet/mining/provider-model.d.ts +30 -0
  64. package/dist/wallet/mining/provider-model.js +134 -0
  65. package/dist/wallet/mining/runner.d.ts +156 -5
  66. package/dist/wallet/mining/runner.js +1019 -399
  67. package/dist/wallet/mining/runtime-artifacts.js +1 -0
  68. package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
  69. package/dist/wallet/mining/sentences.d.ts +2 -2
  70. package/dist/wallet/mining/sentences.js +32 -6
  71. package/dist/wallet/mining/types.d.ts +35 -1
  72. package/dist/wallet/mining/visualizer.d.ts +3 -0
  73. package/dist/wallet/mining/visualizer.js +132 -15
  74. package/dist/wallet/read/context.d.ts +1 -0
  75. package/dist/wallet/read/context.js +15 -7
  76. package/dist/wallet/state/client-password-agent.js +4 -1
  77. package/dist/wallet/state/client-password.js +15 -8
  78. package/dist/wallet/tx/common.js +1 -1
  79. package/package.json +3 -2
@@ -0,0 +1,152 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { compareSemver, parseSemver, } from "../semver.js";
3
+ import { writeJsonFileAtomic } from "../wallet/fs/atomic.js";
4
+ export const UPDATE_CHECK_CACHE_SCHEMA_VERSION = 1;
5
+ export const UPDATE_CHECK_MAX_AGE_MS = 24 * 60 * 60 * 1000;
6
+ export const PASSIVE_UPDATE_CHECK_TIMEOUT_MS = 500;
7
+ export const EXPLICIT_UPDATE_CHECK_TIMEOUT_MS = 5_000;
8
+ export const UPDATE_CHECK_URL = "https://registry.npmjs.org/@cogcoin/client/latest";
9
+ export const CLI_INSTALL_COMMAND = "npm install -g @cogcoin/client";
10
+ export function createEmptyUpdateCheckCache() {
11
+ return {
12
+ schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
13
+ lastCheckedAtUnixMs: 0,
14
+ latestVersion: null,
15
+ lastNotifiedCurrentVersion: null,
16
+ lastNotifiedLatestVersion: null,
17
+ lastNotifiedAtUnixMs: null,
18
+ };
19
+ }
20
+ export { compareSemver, parseSemver };
21
+ export function isUpdateCheckDisabled(env) {
22
+ const raw = env.COGCOIN_DISABLE_UPDATE_CHECK;
23
+ if (raw === undefined) {
24
+ return false;
25
+ }
26
+ const normalized = raw.trim().toLowerCase();
27
+ return normalized === "1" || normalized === "true" || normalized === "yes";
28
+ }
29
+ function normalizeUpdateCheckCache(parsed) {
30
+ if (typeof parsed !== "object" || parsed === null) {
31
+ return null;
32
+ }
33
+ const candidate = parsed;
34
+ if (candidate.schemaVersion !== UPDATE_CHECK_CACHE_SCHEMA_VERSION) {
35
+ return null;
36
+ }
37
+ return {
38
+ schemaVersion: UPDATE_CHECK_CACHE_SCHEMA_VERSION,
39
+ lastCheckedAtUnixMs: typeof candidate.lastCheckedAtUnixMs === "number" ? candidate.lastCheckedAtUnixMs : 0,
40
+ latestVersion: typeof candidate.latestVersion === "string" ? candidate.latestVersion : null,
41
+ lastNotifiedCurrentVersion: typeof candidate.lastNotifiedCurrentVersion === "string"
42
+ ? candidate.lastNotifiedCurrentVersion
43
+ : null,
44
+ lastNotifiedLatestVersion: typeof candidate.lastNotifiedLatestVersion === "string"
45
+ ? candidate.lastNotifiedLatestVersion
46
+ : null,
47
+ lastNotifiedAtUnixMs: typeof candidate.lastNotifiedAtUnixMs === "number"
48
+ ? candidate.lastNotifiedAtUnixMs
49
+ : null,
50
+ lastCheckErrorKind: typeof candidate.lastCheckErrorKind === "string"
51
+ ? candidate.lastCheckErrorKind
52
+ : undefined,
53
+ };
54
+ }
55
+ export async function loadUpdateCheckCache(cachePath) {
56
+ try {
57
+ const raw = await readFile(cachePath, "utf8");
58
+ return normalizeUpdateCheckCache(JSON.parse(raw));
59
+ }
60
+ catch (error) {
61
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
62
+ return null;
63
+ }
64
+ return null;
65
+ }
66
+ }
67
+ export function shouldRefreshUpdateCheck(cache, now) {
68
+ return now - cache.lastCheckedAtUnixMs >= UPDATE_CHECK_MAX_AGE_MS;
69
+ }
70
+ export function applyUpdateCheckResult(cache, result, now) {
71
+ return {
72
+ ...cache,
73
+ lastCheckedAtUnixMs: now,
74
+ latestVersion: result.kind === "success" ? result.latestVersion : cache.latestVersion,
75
+ lastCheckErrorKind: result.kind === "success" ? undefined : result.errorKind,
76
+ };
77
+ }
78
+ export function recordUpdateNotification(cache, currentVersion, latestVersion, now) {
79
+ return {
80
+ ...cache,
81
+ lastNotifiedCurrentVersion: currentVersion,
82
+ lastNotifiedLatestVersion: latestVersion,
83
+ lastNotifiedAtUnixMs: now,
84
+ };
85
+ }
86
+ export async function fetchLatestPublishedVersion(fetchImpl, options = {}) {
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(() => {
89
+ controller.abort();
90
+ }, options.timeoutMs ?? PASSIVE_UPDATE_CHECK_TIMEOUT_MS);
91
+ try {
92
+ const response = await fetchImpl(UPDATE_CHECK_URL, {
93
+ headers: {
94
+ accept: "application/json",
95
+ },
96
+ signal: controller.signal,
97
+ });
98
+ if (!response.ok) {
99
+ return {
100
+ kind: "failure",
101
+ errorKind: `http_${response.status}`,
102
+ };
103
+ }
104
+ let payload;
105
+ try {
106
+ payload = await response.json();
107
+ }
108
+ catch {
109
+ return {
110
+ kind: "failure",
111
+ errorKind: "invalid_json",
112
+ };
113
+ }
114
+ const latestVersion = typeof payload.version === "string"
115
+ ? payload.version
116
+ : null;
117
+ if (latestVersion === null) {
118
+ return {
119
+ kind: "failure",
120
+ errorKind: "invalid_payload",
121
+ };
122
+ }
123
+ if (parseSemver(latestVersion) === null) {
124
+ return {
125
+ kind: "failure",
126
+ errorKind: "invalid_semver",
127
+ };
128
+ }
129
+ return {
130
+ kind: "success",
131
+ latestVersion,
132
+ };
133
+ }
134
+ catch (error) {
135
+ if (error instanceof Error && error.name === "AbortError") {
136
+ return {
137
+ kind: "failure",
138
+ errorKind: "timeout",
139
+ };
140
+ }
141
+ return {
142
+ kind: "failure",
143
+ errorKind: "network",
144
+ };
145
+ }
146
+ finally {
147
+ clearTimeout(timer);
148
+ }
149
+ }
150
+ export async function persistUpdateCheckCache(cachePath, cache) {
151
+ await writeJsonFileAtomic(cachePath, cache);
152
+ }
@@ -22,6 +22,9 @@ export async function initializeState(store, genesisParameters) {
22
22
  if (tip !== null) {
23
23
  throw new Error("client_store_tip_without_snapshot");
24
24
  }
25
+ // Repair orphaned rewind rows from previously interrupted writers so the
26
+ // next replay pass does not collide on a stale future height.
27
+ await store.deleteBlockRecordsAbove(-1);
25
28
  return {
26
29
  state: createInitialState(genesisParameters),
27
30
  tip: null,
@@ -36,6 +39,7 @@ export async function initializeState(store, genesisParameters) {
36
39
  };
37
40
  }
38
41
  if (tip === null) {
42
+ await store.deleteBlockRecordsAbove(snapshot.height);
39
43
  return {
40
44
  state,
41
45
  tip: {
@@ -49,5 +53,6 @@ export async function initializeState(store, genesisParameters) {
49
53
  if (tip.height !== snapshot.height || tip.blockHashHex !== snapshot.blockHashHex) {
50
54
  throw new Error("client_store_snapshot_tip_mismatch");
51
55
  }
56
+ await store.deleteBlockRecordsAbove(tip.height);
52
57
  return { state, tip };
53
58
  }
@@ -0,0 +1,12 @@
1
+ export interface ParsedSemver {
2
+ major: number;
3
+ minor: number;
4
+ patch: number;
5
+ prerelease: Array<{
6
+ raw: string;
7
+ numeric: boolean;
8
+ numericValue: number | null;
9
+ }>;
10
+ }
11
+ export declare function parseSemver(version: string): ParsedSemver | null;
12
+ export declare function compareSemver(left: string, right: string): number | null;
package/dist/semver.js ADDED
@@ -0,0 +1,68 @@
1
+ export function parseSemver(version) {
2
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version.trim());
3
+ if (match === null) {
4
+ return null;
5
+ }
6
+ const prerelease = match[4] === undefined
7
+ ? []
8
+ : match[4].split(".").map((raw) => ({
9
+ raw,
10
+ numeric: /^(0|[1-9]\d*)$/.test(raw),
11
+ numericValue: /^(0|[1-9]\d*)$/.test(raw) ? Number(raw) : null,
12
+ }));
13
+ return {
14
+ major: Number(match[1]),
15
+ minor: Number(match[2]),
16
+ patch: Number(match[3]),
17
+ prerelease,
18
+ };
19
+ }
20
+ export function compareSemver(left, right) {
21
+ const leftParsed = parseSemver(left);
22
+ const rightParsed = parseSemver(right);
23
+ if (leftParsed === null || rightParsed === null) {
24
+ return null;
25
+ }
26
+ if (leftParsed.major !== rightParsed.major) {
27
+ return leftParsed.major > rightParsed.major ? 1 : -1;
28
+ }
29
+ if (leftParsed.minor !== rightParsed.minor) {
30
+ return leftParsed.minor > rightParsed.minor ? 1 : -1;
31
+ }
32
+ if (leftParsed.patch !== rightParsed.patch) {
33
+ return leftParsed.patch > rightParsed.patch ? 1 : -1;
34
+ }
35
+ if (leftParsed.prerelease.length === 0 && rightParsed.prerelease.length === 0) {
36
+ return 0;
37
+ }
38
+ if (leftParsed.prerelease.length === 0) {
39
+ return 1;
40
+ }
41
+ if (rightParsed.prerelease.length === 0) {
42
+ return -1;
43
+ }
44
+ const maxLength = Math.max(leftParsed.prerelease.length, rightParsed.prerelease.length);
45
+ for (let index = 0; index < maxLength; index += 1) {
46
+ const leftIdentifier = leftParsed.prerelease[index];
47
+ const rightIdentifier = rightParsed.prerelease[index];
48
+ if (leftIdentifier === undefined) {
49
+ return -1;
50
+ }
51
+ if (rightIdentifier === undefined) {
52
+ return 1;
53
+ }
54
+ if (leftIdentifier.numeric && rightIdentifier.numeric) {
55
+ if (leftIdentifier.numericValue !== rightIdentifier.numericValue) {
56
+ return leftIdentifier.numericValue > rightIdentifier.numericValue ? 1 : -1;
57
+ }
58
+ continue;
59
+ }
60
+ if (leftIdentifier.numeric !== rightIdentifier.numeric) {
61
+ return leftIdentifier.numeric ? -1 : 1;
62
+ }
63
+ if (leftIdentifier.raw !== rightIdentifier.raw) {
64
+ return leftIdentifier.raw > rightIdentifier.raw ? 1 : -1;
65
+ }
66
+ }
67
+ return 0;
68
+ }
@@ -10,6 +10,16 @@ export interface WalletPrompter {
10
10
  writeLine(message: string): void;
11
11
  prompt(message: string): Promise<string>;
12
12
  promptHidden?(message: string): Promise<string>;
13
+ selectOption?(options: {
14
+ message: string;
15
+ options: Array<{
16
+ label: string;
17
+ description?: string | null;
18
+ value: string;
19
+ }>;
20
+ initialValue?: string | null;
21
+ footer?: string | null;
22
+ }): Promise<string>;
13
23
  clearSensitiveDisplay?(scope: "mnemonic-reveal" | "restore-mnemonic-entry"): void | Promise<void>;
14
24
  }
15
25
  export interface WalletInitializationResult {
@@ -1,18 +1,74 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { writeJsonFileAtomic } from "../fs/atomic.js";
3
3
  import { decryptJsonWithSecretProvider, encryptJsonWithSecretProvider } from "../state/crypto.js";
4
+ import { normalizeMiningProviderConfigRecord } from "./provider-model.js";
5
+ const MINING_PROVIDER_KINDS = ["openai", "anthropic"];
6
+ function normalizeDomainExtraPrompts(raw) {
7
+ if (raw === null || typeof raw !== "object") {
8
+ return {};
9
+ }
10
+ const normalized = new Map();
11
+ for (const [rawDomainName, rawPrompt] of Object.entries(raw)) {
12
+ if (typeof rawPrompt !== "string") {
13
+ continue;
14
+ }
15
+ const domainName = rawDomainName.trim().toLowerCase();
16
+ const prompt = rawPrompt.trim();
17
+ if (domainName.length === 0 || prompt.length === 0) {
18
+ continue;
19
+ }
20
+ normalized.set(domainName, prompt);
21
+ }
22
+ return Object.fromEntries([...normalized.entries()].sort(([left], [right]) => left.localeCompare(right)));
23
+ }
4
24
  function createEmptyClientConfig() {
5
25
  return {
6
26
  schemaVersion: 1,
7
27
  mining: {
8
28
  builtIn: null,
29
+ domainExtraPrompts: {},
30
+ },
31
+ };
32
+ }
33
+ function normalizeBuiltInProviderConfigMap(raw) {
34
+ if (raw === null || typeof raw !== "object") {
35
+ return {};
36
+ }
37
+ const normalized = {};
38
+ const entries = raw;
39
+ for (const provider of MINING_PROVIDER_KINDS) {
40
+ const config = entries[provider];
41
+ if (config == null) {
42
+ continue;
43
+ }
44
+ normalized[provider] = normalizeMiningProviderConfigRecord({
45
+ ...config,
46
+ provider,
47
+ });
48
+ }
49
+ return normalized;
50
+ }
51
+ function normalizeClientConfig(config) {
52
+ const mining = config.mining ?? createEmptyClientConfig().mining;
53
+ const builtIn = mining.builtIn === null ? null : normalizeMiningProviderConfigRecord(mining.builtIn);
54
+ const builtInByProvider = normalizeBuiltInProviderConfigMap(mining.builtInByProvider);
55
+ if (builtIn !== null) {
56
+ builtInByProvider[builtIn.provider] = builtIn;
57
+ }
58
+ return {
59
+ ...config,
60
+ mining: {
61
+ ...mining,
62
+ builtIn,
63
+ ...(Object.keys(builtInByProvider).length === 0 ? {} : { builtInByProvider }),
64
+ domainExtraPrompts: normalizeDomainExtraPrompts(mining.domainExtraPrompts),
9
65
  },
10
66
  };
11
67
  }
12
68
  export async function loadClientConfig(options) {
13
69
  try {
14
70
  const raw = await readFile(options.path, "utf8");
15
- return await decryptJsonWithSecretProvider(JSON.parse(raw), options.provider);
71
+ return normalizeClientConfig(await decryptJsonWithSecretProvider(JSON.parse(raw), options.provider));
16
72
  }
17
73
  catch (error) {
18
74
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
@@ -22,7 +78,7 @@ export async function loadClientConfig(options) {
22
78
  }
23
79
  }
24
80
  export async function saveClientConfig(options) {
25
- const envelope = await encryptJsonWithSecretProvider(options.config, options.provider, options.secretReference, {
81
+ const envelope = await encryptJsonWithSecretProvider(normalizeClientConfig(options.config), options.provider, options.secretReference, {
26
82
  format: "cogcoin-client-config",
27
83
  });
28
84
  await writeJsonFileAtomic(options.path, envelope, { mode: 0o600 });
@@ -33,7 +89,12 @@ export async function saveBuiltInMiningProviderConfig(options) {
33
89
  provider: options.provider,
34
90
  }).catch(() => null);
35
91
  const nextConfig = existing ?? createEmptyClientConfig();
36
- nextConfig.mining.builtIn = options.config;
92
+ const normalizedConfig = normalizeMiningProviderConfigRecord(options.config);
93
+ nextConfig.mining.builtIn = normalizedConfig;
94
+ nextConfig.mining.builtInByProvider = {
95
+ ...(nextConfig.mining.builtInByProvider ?? {}),
96
+ [normalizedConfig.provider]: normalizedConfig,
97
+ };
37
98
  await saveClientConfig({
38
99
  path: options.path,
39
100
  provider: options.provider,
@@ -2,7 +2,7 @@ import type { WalletPrompter } from "../lifecycle.js";
2
2
  import { type WalletRuntimePaths } from "../runtime.js";
3
3
  import { type WalletSecretProvider } from "../state/provider.js";
4
4
  import type { WalletBitcoindStatus, WalletIndexerStatus, WalletLocalStateStatus, WalletNodeStatus } from "../read/types.js";
5
- import type { MiningControlPlaneView, MiningEventRecord, MiningRuntimeStatusV1 } from "./types.js";
5
+ import type { MiningControlPlaneView, MiningEventRecord, MiningProviderConfigByProvider, MiningProviderConfigRecord, MiningRuntimeStatusV1 } from "./types.js";
6
6
  export declare function inspectMiningControlPlane(options: {
7
7
  provider?: WalletSecretProvider;
8
8
  localState: WalletLocalStateStatus;
@@ -23,6 +23,10 @@ export declare function refreshMiningRuntimeStatus(options: {
23
23
  nowUnixMs?: number;
24
24
  paths?: WalletRuntimePaths;
25
25
  }): Promise<MiningControlPlaneView>;
26
+ export declare function promptForMiningProviderConfigForTesting(prompter: WalletPrompter, eligibleRootCount: number, options?: {
27
+ currentConfig?: MiningProviderConfigRecord | null;
28
+ rememberedConfigs?: MiningProviderConfigByProvider;
29
+ }): Promise<MiningProviderConfigRecord>;
26
30
  export declare function setupBuiltInMining(options: {
27
31
  provider?: WalletSecretProvider;
28
32
  prompter: WalletPrompter;