forkit-ai-footprints 0.2.0 → 0.2.4

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 (120) hide show
  1. package/PRIVACY.md +57 -27
  2. package/README.md +43 -41
  3. package/STATUS.md +92 -37
  4. package/dist/activity-page.d.ts +13 -0
  5. package/dist/activity-page.js +105 -0
  6. package/dist/activity-view-model.d.ts +154 -0
  7. package/dist/activity-view-model.js +364 -0
  8. package/dist/agents.d.ts +2 -0
  9. package/dist/agents.js +108 -6
  10. package/dist/bootstrap/.metadata_never_index +0 -0
  11. package/dist/bootstrap/Forkit AI Footprint.app.zip +0 -0
  12. package/dist/cli.js +91 -14
  13. package/dist/device-presence.d.ts +17 -0
  14. package/dist/device-presence.js +64 -0
  15. package/dist/global-preview.d.ts +34 -0
  16. package/dist/global-preview.js +209 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +5 -1
  19. package/dist/insights.d.ts +1 -1
  20. package/dist/insights.js +10 -8
  21. package/dist/local-observation-state.d.ts +43 -0
  22. package/dist/local-observation-state.js +124 -0
  23. package/dist/locales.json +564 -108
  24. package/dist/localization.d.ts +564 -108
  25. package/dist/localization.js +40 -30
  26. package/dist/monitor.d.ts +38 -2
  27. package/dist/monitor.js +257 -19
  28. package/dist/runtime-activity.d.ts +4 -1
  29. package/dist/runtime-activity.js +21 -5
  30. package/dist/server.d.ts +5 -0
  31. package/dist/server.js +160 -17
  32. package/dist/share-page.d.ts +2 -1
  33. package/dist/share-page.js +16 -52
  34. package/dist/sharing.d.ts +12 -1
  35. package/dist/sharing.js +59 -1
  36. package/dist/types.d.ts +1 -1
  37. package/dist/workflow-context.d.ts +13 -0
  38. package/dist/workflow-context.js +144 -0
  39. package/docs/GLOBAL_DISTRIBUTION.md +7 -3
  40. package/docs/MACOS_ACCURACY_GATE.md +3 -3
  41. package/docs/MACOS_TESTER_GUIDE.md +11 -10
  42. package/package.json +6 -6
  43. package/release/channels.json +2 -2
  44. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Info.plist +0 -5
  45. package/dist/bootstrap/Forkit AI Footprint.app/Contents/MacOS/Forkit AI Footprint +0 -0
  46. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/ForkitAIFootprint.icns +0 -0
  47. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/ForkitStatusTemplate.png +0 -0
  48. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/agents.d.ts +0 -22
  49. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/agents.js +0 -281
  50. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/aggregate.d.ts +0 -28
  51. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/aggregate.js +0 -151
  52. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/assets/forkit-icon-dark.png +0 -0
  53. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/assets/forkit-icon-light.png +0 -0
  54. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/census.d.ts +0 -3
  55. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/census.js +0 -196
  56. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/cli.d.ts +0 -32
  57. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/cli.js +0 -404
  58. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/doctor.d.ts +0 -14
  59. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/doctor.js +0 -71
  60. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/endpoints.d.ts +0 -8
  61. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/endpoints.js +0 -45
  62. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/evaluate.d.ts +0 -44
  63. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/evaluate.js +0 -102
  64. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/filesystem.d.ts +0 -11
  65. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/filesystem.js +0 -226
  66. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/format.d.ts +0 -7
  67. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/format.js +0 -78
  68. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/hash.d.ts +0 -4
  69. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/hash.js +0 -16
  70. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/index.d.ts +0 -22
  71. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/index.js +0 -58
  72. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/insights.d.ts +0 -13
  73. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/insights.js +0 -76
  74. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/local-device.d.ts +0 -19
  75. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/local-device.js +0 -81
  76. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/locales.json +0 -272
  77. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/localization.d.ts +0 -277
  78. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/localization.js +0 -55
  79. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/mcp.d.ts +0 -9
  80. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/mcp.js +0 -93
  81. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/monitor.d.ts +0 -99
  82. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/monitor.js +0 -285
  83. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/base.d.ts +0 -34
  84. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/base.js +0 -88
  85. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/index.d.ts +0 -6
  86. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/index.js +0 -24
  87. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/lmstudio.d.ts +0 -9
  88. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/lmstudio.js +0 -71
  89. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/ollama.d.ts +0 -12
  90. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/ollama.js +0 -171
  91. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/openai-compatible.d.ts +0 -10
  92. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/providers/openai-compatible.js +0 -68
  93. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/resource-evidence.d.ts +0 -17
  94. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/resource-evidence.js +0 -52
  95. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/runtime-activity.d.ts +0 -5
  96. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/runtime-activity.js +0 -66
  97. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/runtime-identity.d.ts +0 -7
  98. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/runtime-identity.js +0 -66
  99. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/server.d.ts +0 -21
  100. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/server.js +0 -308
  101. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/share-page.d.ts +0 -75
  102. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/share-page.js +0 -167
  103. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/sharing.d.ts +0 -35
  104. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/sharing.js +0 -67
  105. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/tools.d.ts +0 -9
  106. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/tools.js +0 -106
  107. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/types.d.ts +0 -166
  108. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/types.js +0 -3
  109. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/version.d.ts +0 -4
  110. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/dist/version.js +0 -13
  111. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/node_modules/ps-list/index.d.ts +0 -76
  112. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/node_modules/ps-list/index.js +0 -366
  113. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/node_modules/ps-list/license +0 -9
  114. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/node_modules/ps-list/package.json +0 -44
  115. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/node_modules/ps-list/readme.md +0 -56
  116. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe +0 -0
  117. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/node_modules/ps-list/vendor/fastlist-0.3.0-x86.exe +0 -0
  118. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/app/package.json +0 -6
  119. package/dist/bootstrap/Forkit AI Footprint.app/Contents/Resources/runtime/node +0 -0
  120. package/dist/bootstrap/Forkit AI Footprint.app/Contents/_CodeSignature/CodeResources +0 -931
package/dist/cli.js CHANGED
@@ -40,8 +40,8 @@ Options:
40
40
  --verbose Show detector and identity details
41
41
  --guess <count> Compare your estimate with discovered models
42
42
  --copy Copy the rendered local result to the clipboard
43
- --anonymous-payload Reserved for a completed 10-minute monitor session
44
- --consent-share Separate consent; never enables upload in this build
43
+ --anonymous-payload Reserved for a completed one-hour monitor session
44
+ --consent-share Prepare an aggregate payload; native-app consent controls Preview sync
45
45
  --output <file> Save the selected human or JSON report
46
46
  --model-dir <path> Inspect an explicit model directory; repeatable
47
47
  --truth <file> Evaluate locally against manually labelled macOS truth
@@ -55,9 +55,10 @@ Options:
55
55
  -h, --help Show this help
56
56
 
57
57
  Privacy:
58
- AI Footprints reads metadata only. It does not read model bytes, retain raw process
59
- commands or MCP configuration values, authenticate to Forkit.dev, or write to
60
- any passport, registry, Runtime_C2, or production service.
58
+ AI Footprints reads metadata only. It does not read model bytes or retain raw
59
+ process commands or MCP configuration values. It does not write to any passport,
60
+ registry, or Runtime_C2 service. The installed app may sync only the allowlisted anonymous
61
+ Global AI Preview aggregate after explicit v2 consent and one valid hour.
61
62
  `;
62
63
  function parseArgs(args) {
63
64
  let command = 'install';
@@ -186,12 +187,65 @@ function parseArgs(args) {
186
187
  port,
187
188
  };
188
189
  }
190
+ const FORKIT_APP_NAME = 'Forkit AI Footprint.app';
191
+ const FORKIT_BUNDLE_IDENTIFIER = 'dev.forkit.ai-footprints';
192
+ const LAUNCH_SERVICES = '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister';
193
+ async function isForkitApp(appPath) {
194
+ const info = await promises_1.default.readFile(node_path_1.default.join(appPath, 'Contents', 'Info.plist'), 'utf8').catch(() => '');
195
+ return info.includes(FORKIT_BUNDLE_IDENTIFIER);
196
+ }
197
+ async function recognizedForkitApps(applicationsDirectory) {
198
+ const entries = await promises_1.default.readdir(applicationsDirectory, { withFileTypes: true }).catch(() => []);
199
+ const apps = [];
200
+ for (const entry of entries) {
201
+ if (!entry.isDirectory() || !entry.name.endsWith('.app') || entry.name.startsWith('.'))
202
+ continue;
203
+ const candidate = node_path_1.default.join(applicationsDirectory, entry.name);
204
+ if (await isForkitApp(candidate))
205
+ apps.push(candidate);
206
+ }
207
+ return apps;
208
+ }
209
+ function stopRunningForkitApps(appPaths) {
210
+ const snapshot = (0, node_child_process_1.spawnSync)('/bin/ps', ['-axo', 'pid=,ppid=,comm='], { encoding: 'utf8', shell: false });
211
+ if (snapshot.status !== 0 || !snapshot.stdout)
212
+ return;
213
+ const processes = snapshot.stdout.split('\n').map((line) => {
214
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
215
+ return match ? { pid: Number(match[1]), parent: Number(match[2]), command: match[3] } : null;
216
+ }).filter((entry) => Boolean(entry));
217
+ const matching = processes.filter((entry) => appPaths.some((appPath) => entry.command.startsWith(`${appPath}/Contents/`)));
218
+ if (matching.length === 0)
219
+ return;
220
+ const matchingPids = new Set(matching.map((entry) => entry.pid));
221
+ const ordered = [...matching].sort((left, right) => Number(matchingPids.has(right.parent)) - Number(matchingPids.has(left.parent)));
222
+ for (const entry of ordered) {
223
+ try {
224
+ process.kill(entry.pid, 'SIGTERM');
225
+ }
226
+ catch { /* process already ended */ }
227
+ }
228
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 800);
229
+ for (const entry of ordered) {
230
+ try {
231
+ process.kill(entry.pid, 0);
232
+ process.kill(entry.pid, 'SIGKILL');
233
+ }
234
+ catch { /* process ended cleanly */ }
235
+ }
236
+ }
237
+ function unregisterApp(appPath) {
238
+ (0, node_child_process_1.spawnSync)(LAUNCH_SERVICES, ['-u', appPath], { stdio: 'ignore', shell: false });
239
+ }
240
+ function registerApp(appPath) {
241
+ (0, node_child_process_1.spawnSync)(LAUNCH_SERVICES, ['-f', appPath], { stdio: 'ignore', shell: false });
242
+ }
189
243
  async function installPersistentMacApp(options = {}) {
190
244
  if (process.platform !== 'darwin' || process.arch !== 'arm64')
191
245
  throw new Error('The persistent GUI currently supports Apple Silicon macOS only.');
192
- const source = node_path_1.default.join(__dirname, 'bootstrap', 'Forkit AI Footprint.app');
246
+ const source = node_path_1.default.join(__dirname, 'bootstrap', `${FORKIT_APP_NAME}.zip`);
193
247
  try {
194
- await promises_1.default.access(node_path_1.default.join(source, 'Contents', 'Info.plist'));
248
+ await promises_1.default.access(source);
195
249
  }
196
250
  catch {
197
251
  throw new Error('This package does not contain the macOS GUI bootstrap. Install from the packed release candidate.');
@@ -199,32 +253,55 @@ async function installPersistentMacApp(options = {}) {
199
253
  const applicationsDirectory = options.applicationsDirectory
200
254
  ?? process.env.FORKIT_AI_FOOTPRINTS_APPLICATIONS_DIR
201
255
  ?? node_path_1.default.join(node_os_1.default.homedir(), 'Applications');
202
- const destination = node_path_1.default.join(applicationsDirectory, 'Forkit AI Footprint.app');
256
+ const destination = node_path_1.default.join(applicationsDirectory, FORKIT_APP_NAME);
203
257
  const staging = node_path_1.default.join(applicationsDirectory, `.Forkit AI Footprint.installing-${process.pid}.app`);
204
258
  const previous = node_path_1.default.join(applicationsDirectory, `.Forkit AI Footprint.previous-${process.pid}.app`);
259
+ const extraction = node_path_1.default.join(applicationsDirectory, `.Forkit AI Footprint.extracting-${process.pid}`);
260
+ const shouldOpen = options.open !== false && process.env.FORKIT_AI_FOOTPRINTS_NO_OPEN !== '1';
205
261
  await promises_1.default.mkdir(applicationsDirectory, { recursive: true, mode: 0o700 });
262
+ const recognizedApps = await recognizedForkitApps(applicationsDirectory);
263
+ stopRunningForkitApps(recognizedApps);
206
264
  await promises_1.default.rm(staging, { recursive: true, force: true });
207
- await promises_1.default.cp(source, staging, { recursive: true, preserveTimestamps: true });
265
+ await promises_1.default.rm(extraction, { recursive: true, force: true });
266
+ await promises_1.default.mkdir(extraction, { recursive: true, mode: 0o700 });
267
+ const unpacked = (0, node_child_process_1.spawnSync)('/usr/bin/ditto', ['-x', '-k', source, extraction], { stdio: 'ignore', shell: false });
268
+ const extractedApp = node_path_1.default.join(extraction, FORKIT_APP_NAME);
269
+ if (unpacked.status !== 0 || !(await isForkitApp(extractedApp))) {
270
+ await promises_1.default.rm(extraction, { recursive: true, force: true });
271
+ throw new Error('The packaged macOS app has an unexpected identity.');
272
+ }
273
+ await promises_1.default.rename(extractedApp, staging);
274
+ await promises_1.default.rm(extraction, { recursive: true, force: true });
208
275
  let hadPrevious = false;
209
276
  try {
210
277
  const existingInfo = await promises_1.default.readFile(node_path_1.default.join(destination, 'Contents', 'Info.plist'), 'utf8').catch(() => '');
211
- if (existingInfo && !existingInfo.includes('dev.forkit.ai-footprints'))
278
+ if (existingInfo && !existingInfo.includes(FORKIT_BUNDLE_IDENTIFIER))
212
279
  throw new Error('The destination contains a different application.');
213
280
  if (existingInfo) {
281
+ unregisterApp(destination);
214
282
  await promises_1.default.rename(destination, previous);
215
283
  hadPrevious = true;
216
284
  }
217
285
  await promises_1.default.rename(staging, destination);
218
286
  if (hadPrevious)
219
287
  await promises_1.default.rm(previous, { recursive: true, force: true });
288
+ for (const duplicate of recognizedApps) {
289
+ if (duplicate === destination)
290
+ continue;
291
+ unregisterApp(duplicate);
292
+ await promises_1.default.rm(duplicate, { recursive: true, force: true });
293
+ }
294
+ if (shouldOpen)
295
+ registerApp(destination);
220
296
  }
221
297
  catch (error) {
222
298
  await promises_1.default.rm(staging, { recursive: true, force: true });
299
+ await promises_1.default.rm(extraction, { recursive: true, force: true });
223
300
  if (hadPrevious)
224
301
  await promises_1.default.rename(previous, destination).catch(() => undefined);
225
302
  throw error;
226
303
  }
227
- if (options.open !== false && process.env.FORKIT_AI_FOOTPRINTS_NO_OPEN !== '1') {
304
+ if (shouldOpen) {
228
305
  const launched = (0, node_child_process_1.spawnSync)('/usr/bin/open', [destination], { stdio: 'ignore', shell: false });
229
306
  if (launched.status !== 0)
230
307
  throw new Error('Forkit was installed, but macOS could not open it.');
@@ -278,7 +355,7 @@ async function main(args = process.argv.slice(2)) {
278
355
  }
279
356
  if (options.command === 'install') {
280
357
  const destination = await installPersistentMacApp();
281
- process.stdout.write(`Forkit AI Footprint is installed at ${destination}\nOpen it later from Applications or Spotlight; Terminal is no longer required.\nYour AI activity stays on this device by default. After seeing your results, you can optionally review aggregate measurements for global comparison.\n`);
358
+ process.stdout.write(`Forkit AI Footprint is installed at ${destination}\nOpen it later from Applications or Spotlight; Terminal is no longer required.\nOn first launch, Forkit asks whether an aggregate-only measurement may be contributed to the Global AI Preview after one valid observed hour. Nothing is sent unless the person explicitly agrees.\n`);
282
359
  return 0;
283
360
  }
284
361
  if (options.command === 'serve') {
@@ -310,7 +387,7 @@ async function main(args = process.argv.slice(2)) {
310
387
  : `Observed ${summary.observed_seconds.toFixed(1)}s · AI active ${summary.active_seconds.toFixed(1)}s · ratio ${summary.activity_ratio === null ? 'unavailable' : `${Math.round(summary.activity_ratio * 100)}%`}\n`;
311
388
  await emit(rendered, options.output);
312
389
  if (options.anonymousPayload) {
313
- process.stderr.write('Anonymous preview requires a completed 10-minute GUI session with the exact payload reviewed first. Nothing was uploaded.\n');
390
+ process.stderr.write('Anonymous preview requires a completed one-hour GUI session with the exact payload reviewed first. Nothing was uploaded.\n');
314
391
  return 2;
315
392
  }
316
393
  return 0;
@@ -387,7 +464,7 @@ async function main(args = process.argv.slice(2)) {
387
464
  }
388
465
  if (options.anonymousPayload) {
389
466
  process.stderr.write(options.shareConsent
390
- ? 'Anonymous contribution was not prepared: a valid 10-minute monitor session and exact payload review are required. Nothing was uploaded.\n'
467
+ ? 'Anonymous contribution was not prepared: a valid one-hour monitor session and exact payload review are required. Nothing was uploaded.\n'
391
468
  : 'Anonymous contribution was not prepared: separate --consent-share is required. Nothing was uploaded.\n');
392
469
  return 2;
393
470
  }
@@ -0,0 +1,17 @@
1
+ export type DevicePresenceState = 'active' | 'idle' | 'locked' | 'unavailable';
2
+ export interface DevicePresence {
3
+ state: DevicePresenceState;
4
+ idle_seconds: number | null;
5
+ observation_eligible: boolean;
6
+ }
7
+ export interface MacosDevicePresenceOptions {
8
+ idleThresholdSeconds?: number;
9
+ cacheMs?: number;
10
+ now?: () => number;
11
+ readConsole?: () => Promise<string>;
12
+ readHid?: () => Promise<string>;
13
+ }
14
+ export declare function parseMacosConsoleLocked(value: string): boolean | null;
15
+ export declare function parseMacosIdleSeconds(value: string): number | null;
16
+ export declare function createMacosDevicePresenceProvider(options?: MacosDevicePresenceOptions): () => Promise<DevicePresence>;
17
+ //# sourceMappingURL=device-presence.d.ts.map
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseMacosConsoleLocked = parseMacosConsoleLocked;
4
+ exports.parseMacosIdleSeconds = parseMacosIdleSeconds;
5
+ exports.createMacosDevicePresenceProvider = createMacosDevicePresenceProvider;
6
+ const node_child_process_1 = require("node:child_process");
7
+ const node_util_1 = require("node:util");
8
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
9
+ function parseMacosConsoleLocked(value) {
10
+ const match = /"IOConsoleLocked"\s*=\s*(Yes|No)/i.exec(value);
11
+ if (!match)
12
+ return null;
13
+ return match[1].toLowerCase() === 'yes';
14
+ }
15
+ function parseMacosIdleSeconds(value) {
16
+ const match = /"HIDIdleTime"\s*=\s*(\d+)/.exec(value);
17
+ if (!match)
18
+ return null;
19
+ const nanoseconds = Number(match[1]);
20
+ return Number.isFinite(nanoseconds) && nanoseconds >= 0 ? nanoseconds / 1_000_000_000 : null;
21
+ }
22
+ async function readIoreg(args) {
23
+ const { stdout } = await execFileAsync('/usr/sbin/ioreg', args, {
24
+ encoding: 'utf8', maxBuffer: 2_000_000, env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
25
+ });
26
+ return stdout;
27
+ }
28
+ function createMacosDevicePresenceProvider(options = {}) {
29
+ const idleThresholdSeconds = Math.max(15, options.idleThresholdSeconds ?? 60);
30
+ const cacheMs = Math.max(500, options.cacheMs ?? 2_000);
31
+ const now = options.now ?? Date.now;
32
+ const readConsole = options.readConsole ?? (() => readIoreg(['-n', 'Root', '-d1']));
33
+ const readHid = options.readHid ?? (() => readIoreg(['-c', 'IOHIDSystem', '-r', '-d1']));
34
+ let cachedAt = 0;
35
+ let cached = { state: 'unavailable', idle_seconds: null, observation_eligible: false };
36
+ return async () => {
37
+ const current = now();
38
+ if (cachedAt > 0 && current - cachedAt < cacheMs)
39
+ return cached;
40
+ cachedAt = current;
41
+ try {
42
+ const [consoleOutput, hidOutput] = await Promise.all([readConsole(), readHid()]);
43
+ const locked = parseMacosConsoleLocked(consoleOutput);
44
+ const idleSeconds = parseMacosIdleSeconds(hidOutput);
45
+ if (locked === null || idleSeconds === null) {
46
+ cached = { state: 'unavailable', idle_seconds: idleSeconds, observation_eligible: false };
47
+ }
48
+ else if (locked) {
49
+ cached = { state: 'locked', idle_seconds: idleSeconds, observation_eligible: false };
50
+ }
51
+ else if (idleSeconds >= idleThresholdSeconds) {
52
+ cached = { state: 'idle', idle_seconds: idleSeconds, observation_eligible: false };
53
+ }
54
+ else {
55
+ cached = { state: 'active', idle_seconds: idleSeconds, observation_eligible: true };
56
+ }
57
+ }
58
+ catch {
59
+ cached = { state: 'unavailable', idle_seconds: null, observation_eligible: false };
60
+ }
61
+ return cached;
62
+ };
63
+ }
64
+ //# sourceMappingURL=device-presence.js.map
@@ -0,0 +1,34 @@
1
+ import type { AnonymousAiFootprintContribution } from './sharing';
2
+ export declare const GLOBAL_PREVIEW_ENDPOINT = "https://www.forkit.dev/api/v1/ai-footprints";
3
+ export declare const GLOBAL_PREVIEW_SYNC_INTERVAL_MS: number;
4
+ export interface GlobalPreviewStatus {
5
+ state: 'disabled' | 'waiting' | 'syncing' | 'contributed' | 'unavailable';
6
+ last_attempt_at: string | null;
7
+ last_success_at: string | null;
8
+ last_observed_seconds: number;
9
+ last_error: string | null;
10
+ }
11
+ export interface GlobalPreviewContributorOptions {
12
+ stateDirectory?: string;
13
+ endpoint?: string;
14
+ fetchFn?: typeof fetch;
15
+ now?: () => Date;
16
+ minimumSyncIntervalMs?: number;
17
+ }
18
+ export declare class GlobalPreviewContributor {
19
+ private readonly stateDirectory;
20
+ private readonly endpoint;
21
+ private readonly fetchFn;
22
+ private readonly now;
23
+ private readonly minimumSyncIntervalMs;
24
+ private currentStatus;
25
+ private loaded;
26
+ private syncing;
27
+ constructor(options?: GlobalPreviewContributorOptions);
28
+ status(permission: 'granted' | 'declined' | 'unset'): GlobalPreviewStatus;
29
+ private loadReceipt;
30
+ private saveStatus;
31
+ sync(contribution: AnonymousAiFootprintContribution, permission: 'granted' | 'declined' | 'unset'): Promise<GlobalPreviewStatus>;
32
+ private performSync;
33
+ }
34
+ //# sourceMappingURL=global-preview.d.ts.map
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GlobalPreviewContributor = exports.GLOBAL_PREVIEW_SYNC_INTERVAL_MS = exports.GLOBAL_PREVIEW_ENDPOINT = void 0;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const local_device_1 = require("./local-device");
11
+ exports.GLOBAL_PREVIEW_ENDPOINT = 'https://www.forkit.dev/api/v1/ai-footprints';
12
+ exports.GLOBAL_PREVIEW_SYNC_INTERVAL_MS = 15 * 60 * 1000;
13
+ function base64url(value) {
14
+ return value.toString('base64url');
15
+ }
16
+ function stableJson(value) {
17
+ if (Array.isArray(value))
18
+ return `[${value.map(stableJson).join(',')}]`;
19
+ if (value && typeof value === 'object') {
20
+ const record = value;
21
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`;
22
+ }
23
+ return JSON.stringify(value);
24
+ }
25
+ function previewKeyPath(stateDirectory) {
26
+ return node_path_1.default.join(stateDirectory, 'global-preview-key.json');
27
+ }
28
+ function previewReceiptPath(stateDirectory) {
29
+ return node_path_1.default.join(stateDirectory, 'global-preview-receipt.json');
30
+ }
31
+ async function writePrivateJson(target, value) {
32
+ await promises_1.default.mkdir(node_path_1.default.dirname(target), { recursive: true, mode: 0o700 });
33
+ await promises_1.default.chmod(node_path_1.default.dirname(target), 0o700);
34
+ const temporary = `${target}.${process.pid}.tmp`;
35
+ await promises_1.default.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
36
+ await promises_1.default.chmod(temporary, 0o600);
37
+ await promises_1.default.rename(temporary, target);
38
+ await promises_1.default.chmod(target, 0o600);
39
+ }
40
+ function validKeyFile(value) {
41
+ if (!value || typeof value !== 'object')
42
+ return false;
43
+ const key = value;
44
+ return key.schema_version === '1.0'
45
+ && key.algorithm === 'Ed25519'
46
+ && typeof key.public_key === 'string'
47
+ && /^[A-Za-z0-9_-]{40,256}$/.test(key.public_key)
48
+ && typeof key.private_key === 'string'
49
+ && /^[A-Za-z0-9_-]{40,256}$/.test(key.private_key);
50
+ }
51
+ async function loadOrCreateKey(stateDirectory) {
52
+ const target = previewKeyPath(stateDirectory);
53
+ try {
54
+ const value = JSON.parse(await promises_1.default.readFile(target, 'utf8'));
55
+ if (validKeyFile(value))
56
+ return value;
57
+ }
58
+ catch {
59
+ // Missing or invalid local preview identity is replaced before contribution.
60
+ }
61
+ const pair = node_crypto_1.default.generateKeyPairSync('ed25519');
62
+ const value = {
63
+ schema_version: '1.0',
64
+ algorithm: 'Ed25519',
65
+ public_key: base64url(pair.publicKey.export({ type: 'spki', format: 'der' })),
66
+ private_key: base64url(pair.privateKey.export({ type: 'pkcs8', format: 'der' })),
67
+ };
68
+ await writePrivateJson(target, value);
69
+ return value;
70
+ }
71
+ function emptyStatus() {
72
+ return {
73
+ state: 'waiting',
74
+ last_attempt_at: null,
75
+ last_success_at: null,
76
+ last_observed_seconds: 0,
77
+ last_error: null,
78
+ };
79
+ }
80
+ function validReceipt(value) {
81
+ if (!value || typeof value !== 'object')
82
+ return false;
83
+ const receipt = value;
84
+ return receipt.schema_version === '1.0'
85
+ && ['disabled', 'waiting', 'syncing', 'contributed', 'unavailable'].includes(String(receipt.state))
86
+ && (receipt.last_attempt_at === null || typeof receipt.last_attempt_at === 'string')
87
+ && (receipt.last_success_at === null || typeof receipt.last_success_at === 'string')
88
+ && Number.isSafeInteger(receipt.last_observed_seconds)
89
+ && Number(receipt.last_observed_seconds) >= 0
90
+ && (receipt.last_error === null || typeof receipt.last_error === 'string');
91
+ }
92
+ class GlobalPreviewContributor {
93
+ stateDirectory;
94
+ endpoint;
95
+ fetchFn;
96
+ now;
97
+ minimumSyncIntervalMs;
98
+ currentStatus = emptyStatus();
99
+ loaded = false;
100
+ syncing = null;
101
+ constructor(options = {}) {
102
+ this.stateDirectory = options.stateDirectory ?? (0, local_device_1.defaultLocalStateDirectory)();
103
+ this.endpoint = options.endpoint ?? exports.GLOBAL_PREVIEW_ENDPOINT;
104
+ this.fetchFn = options.fetchFn ?? fetch;
105
+ this.now = options.now ?? (() => new Date());
106
+ this.minimumSyncIntervalMs = options.minimumSyncIntervalMs ?? exports.GLOBAL_PREVIEW_SYNC_INTERVAL_MS;
107
+ const endpointUrl = new URL(this.endpoint);
108
+ if (endpointUrl.protocol !== 'https:' || endpointUrl.origin !== 'https://www.forkit.dev' || endpointUrl.pathname !== '/api/v1/ai-footprints') {
109
+ throw new Error('GLOBAL_PREVIEW_ENDPOINT_NOT_ALLOWED');
110
+ }
111
+ }
112
+ status(permission) {
113
+ return permission === 'granted' ? { ...this.currentStatus } : { ...this.currentStatus, state: 'disabled', last_error: null };
114
+ }
115
+ async loadReceipt() {
116
+ if (this.loaded)
117
+ return;
118
+ this.loaded = true;
119
+ try {
120
+ const value = JSON.parse(await promises_1.default.readFile(previewReceiptPath(this.stateDirectory), 'utf8'));
121
+ if (validReceipt(value))
122
+ this.currentStatus = { ...value };
123
+ }
124
+ catch {
125
+ this.currentStatus = emptyStatus();
126
+ }
127
+ }
128
+ async saveStatus() {
129
+ await writePrivateJson(previewReceiptPath(this.stateDirectory), {
130
+ schema_version: '1.0',
131
+ ...this.currentStatus,
132
+ });
133
+ }
134
+ async sync(contribution, permission) {
135
+ if (permission !== 'granted')
136
+ return this.status(permission);
137
+ if (this.syncing)
138
+ return this.syncing;
139
+ this.syncing = this.performSync(contribution).finally(() => { this.syncing = null; });
140
+ return this.syncing;
141
+ }
142
+ async performSync(contribution) {
143
+ await this.loadReceipt();
144
+ const now = this.now();
145
+ const lastAttempt = this.currentStatus.last_attempt_at ? Date.parse(this.currentStatus.last_attempt_at) : 0;
146
+ const observedSeconds = contribution.observation.valid_seconds;
147
+ if (lastAttempt && now.getTime() - lastAttempt < this.minimumSyncIntervalMs && observedSeconds <= this.currentStatus.last_observed_seconds + 60) {
148
+ return { ...this.currentStatus };
149
+ }
150
+ this.currentStatus = { ...this.currentStatus, state: 'syncing', last_attempt_at: now.toISOString(), last_error: null };
151
+ await this.saveStatus();
152
+ try {
153
+ const challengeResponse = await this.fetchFn(`${this.endpoint}/challenges`, {
154
+ method: 'POST',
155
+ headers: { accept: 'application/json', 'content-type': 'application/json' },
156
+ body: JSON.stringify({ purpose: 'preview-contribution' }),
157
+ signal: AbortSignal.timeout(10_000),
158
+ });
159
+ if (!challengeResponse.ok)
160
+ throw new Error(`CHALLENGE_${challengeResponse.status}`);
161
+ const challenge = await challengeResponse.json();
162
+ if (typeof challenge.challengeId !== 'string' || !/^[a-f0-9]{64}$/.test(challenge.challengeId))
163
+ throw new Error('CHALLENGE_INVALID');
164
+ const key = await loadOrCreateKey(this.stateDirectory);
165
+ const clientDataHash = node_crypto_1.default.createHash('sha256').update(`${challenge.challengeId}.${stableJson(contribution)}`).digest();
166
+ const privateKey = node_crypto_1.default.createPrivateKey({ key: Buffer.from(key.private_key, 'base64url'), type: 'pkcs8', format: 'der' });
167
+ const signature = node_crypto_1.default.sign(null, clientDataHash, privateKey);
168
+ const contributionResponse = await this.fetchFn(`${this.endpoint}/contributions`, {
169
+ method: 'POST',
170
+ headers: { accept: 'application/json', 'content-type': 'application/json' },
171
+ body: JSON.stringify({
172
+ consent: true,
173
+ contribution,
174
+ proof: {
175
+ type: 'community-ed25519',
176
+ challenge_id: challenge.challengeId,
177
+ public_key: key.public_key,
178
+ signature: base64url(signature),
179
+ client_data_hash: clientDataHash.toString('hex'),
180
+ },
181
+ }),
182
+ signal: AbortSignal.timeout(10_000),
183
+ });
184
+ if (!contributionResponse.ok)
185
+ throw new Error(`CONTRIBUTION_${contributionResponse.status}`);
186
+ const accepted = await contributionResponse.json();
187
+ if (accepted.accepted !== true || accepted.trustState !== 'preview')
188
+ throw new Error('CONTRIBUTION_NOT_ACCEPTED');
189
+ this.currentStatus = {
190
+ state: 'contributed',
191
+ last_attempt_at: now.toISOString(),
192
+ last_success_at: now.toISOString(),
193
+ last_observed_seconds: observedSeconds,
194
+ last_error: null,
195
+ };
196
+ }
197
+ catch (error) {
198
+ this.currentStatus = {
199
+ ...this.currentStatus,
200
+ state: 'unavailable',
201
+ last_error: error instanceof Error ? error.message.slice(0, 96) : 'GLOBAL_PREVIEW_UNAVAILABLE',
202
+ };
203
+ }
204
+ await this.saveStatus();
205
+ return { ...this.currentStatus };
206
+ }
207
+ }
208
+ exports.GlobalPreviewContributor = GlobalPreviewContributor;
209
+ //# sourceMappingURL=global-preview.js.map
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { detectAgentProducts, listSystemProcesses } from './agents';
2
2
  export { aggregateMacosFieldEvaluationDirectory, aggregateMacosFieldEvaluations } from './aggregate';
3
3
  export { runCensus } from './census';
4
4
  export { authorizeAnonymousAiFootprintContribution, buildAnonymousAiFootprintPreview, MINIMUM_COMPARISON_SECONDS } from './sharing';
5
+ export { GlobalPreviewContributor, GLOBAL_PREVIEW_ENDPOINT, GLOBAL_PREVIEW_SYNC_INTERVAL_MS } from './global-preview';
6
+ export type { GlobalPreviewContributorOptions, GlobalPreviewStatus } from './global-preview';
5
7
  export { ActivityMonitor } from './monitor';
6
8
  export type * from './monitor';
7
9
  export { buildCensusShareSnapshot, renderCensusSharePage } from './share-page';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.evaluateMacosFieldTruthFile = exports.evaluateMacosFieldTruth = exports.detectMacosMajorVersion = exports.formatDoctorReport = exports.formatCensusReport = exports.scanFilesystemModels = exports.parseLoopbackEndpointList = exports.parseLoopbackEndpoint = exports.runDoctor = exports.detectMcpConfigs = exports.detectAiTools = exports.startAiFootprintsServer = exports.buildLocalInsights = exports.renderCensusSharePage = exports.buildCensusShareSnapshot = exports.ActivityMonitor = exports.MINIMUM_COMPARISON_SECONDS = exports.buildAnonymousAiFootprintPreview = exports.authorizeAnonymousAiFootprintContribution = exports.runCensus = exports.aggregateMacosFieldEvaluations = exports.aggregateMacosFieldEvaluationDirectory = exports.listSystemProcesses = exports.detectAgentProducts = void 0;
17
+ exports.evaluateMacosFieldTruthFile = exports.evaluateMacosFieldTruth = exports.detectMacosMajorVersion = exports.formatDoctorReport = exports.formatCensusReport = exports.scanFilesystemModels = exports.parseLoopbackEndpointList = exports.parseLoopbackEndpoint = exports.runDoctor = exports.detectMcpConfigs = exports.detectAiTools = exports.startAiFootprintsServer = exports.buildLocalInsights = exports.renderCensusSharePage = exports.buildCensusShareSnapshot = exports.ActivityMonitor = exports.GLOBAL_PREVIEW_SYNC_INTERVAL_MS = exports.GLOBAL_PREVIEW_ENDPOINT = exports.GlobalPreviewContributor = exports.MINIMUM_COMPARISON_SECONDS = exports.buildAnonymousAiFootprintPreview = exports.authorizeAnonymousAiFootprintContribution = exports.runCensus = exports.aggregateMacosFieldEvaluations = exports.aggregateMacosFieldEvaluationDirectory = exports.listSystemProcesses = exports.detectAgentProducts = void 0;
18
18
  var agents_1 = require("./agents");
19
19
  Object.defineProperty(exports, "detectAgentProducts", { enumerable: true, get: function () { return agents_1.detectAgentProducts; } });
20
20
  Object.defineProperty(exports, "listSystemProcesses", { enumerable: true, get: function () { return agents_1.listSystemProcesses; } });
@@ -27,6 +27,10 @@ var sharing_1 = require("./sharing");
27
27
  Object.defineProperty(exports, "authorizeAnonymousAiFootprintContribution", { enumerable: true, get: function () { return sharing_1.authorizeAnonymousAiFootprintContribution; } });
28
28
  Object.defineProperty(exports, "buildAnonymousAiFootprintPreview", { enumerable: true, get: function () { return sharing_1.buildAnonymousAiFootprintPreview; } });
29
29
  Object.defineProperty(exports, "MINIMUM_COMPARISON_SECONDS", { enumerable: true, get: function () { return sharing_1.MINIMUM_COMPARISON_SECONDS; } });
30
+ var global_preview_1 = require("./global-preview");
31
+ Object.defineProperty(exports, "GlobalPreviewContributor", { enumerable: true, get: function () { return global_preview_1.GlobalPreviewContributor; } });
32
+ Object.defineProperty(exports, "GLOBAL_PREVIEW_ENDPOINT", { enumerable: true, get: function () { return global_preview_1.GLOBAL_PREVIEW_ENDPOINT; } });
33
+ Object.defineProperty(exports, "GLOBAL_PREVIEW_SYNC_INTERVAL_MS", { enumerable: true, get: function () { return global_preview_1.GLOBAL_PREVIEW_SYNC_INTERVAL_MS; } });
30
34
  var monitor_1 = require("./monitor");
31
35
  Object.defineProperty(exports, "ActivityMonitor", { enumerable: true, get: function () { return monitor_1.ActivityMonitor; } });
32
36
  var share_page_1 = require("./share-page");
@@ -1,6 +1,6 @@
1
1
  import type { MonitorSnapshot } from './monitor';
2
2
  import { type UiLocale } from './localization';
3
- export type LocalInsightKind = 'activity-share' | 'longest-block' | 'workflow' | 'local-models-unused' | 'mostly-idle';
3
+ export type LocalInsightKind = 'activity-share' | 'longest-block' | 'workflow' | 'local-models-unused' | 'mostly-idle' | 'hosted-local';
4
4
  export interface LocalInsight {
5
5
  kind: LocalInsightKind;
6
6
  text: string;
package/dist/insights.js CHANGED
@@ -9,7 +9,9 @@ function durationLabel(value, locale) {
9
9
  return locale === 'de' ? `${seconds} Sek` : `${seconds}s`;
10
10
  if (seconds < 3600)
11
11
  return locale === 'de' ? `${Math.floor(seconds / 60)} Min ${seconds % 60} Sek` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
12
- return locale === 'de' ? `${Math.floor(seconds / 3600)} Std ${Math.floor((seconds % 3600) / 60)} Min` : `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
12
+ return locale === 'de'
13
+ ? `${Math.floor(seconds / 3600)} Std ${Math.floor((seconds % 3600) / 60)} Min ${seconds % 60} Sek`
14
+ : `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m ${seconds % 60}s`;
13
15
  }
14
16
  function longestActiveSeconds(timeline, gapToleranceMs) {
15
17
  let longest = 0;
@@ -54,22 +56,22 @@ function buildLocalInsights(monitor, footprint, minimumSeconds = MINIMUM_INSIGHT
54
56
  insights.push({ kind: 'mostly-idle', text: (0, localization_1.uiText)(locale, 'insightMostlyIdle') });
55
57
  }
56
58
  else if (activeProducts.length === 1) {
57
- insights.push({ kind: 'activity-share', text: (0, localization_1.uiText)(locale, 'insightActivityProduct', { product: activeProducts[0].name, percent: activityPercent }) });
59
+ insights.push({ kind: 'workflow', text: (0, localization_1.uiText)(locale, 'insightFocused', { product: activeProducts[0].name }) });
58
60
  }
59
61
  else {
60
62
  insights.push({ kind: 'activity-share', text: (0, localization_1.uiText)(locale, 'insightActivityGeneric', { percent: activityPercent }) });
61
63
  }
62
64
  const longest = longestActiveSeconds(monitor.timeline, monitor.sample_interval_ms * 3);
63
65
  if (longest >= 10)
64
- insights.push({ kind: 'longest-block', text: (0, localization_1.uiText)(locale, 'insightLongest', { duration: durationLabel(longest, locale) }) });
65
- if (activeProducts.length > 1) {
66
+ insights.push({ kind: 'longest-block', text: (0, localization_1.uiText)(locale, 'insightDeepWork', { duration: durationLabel(longest, locale) }) });
67
+ if (footprint.confirmed_running_model_count > 0) {
68
+ insights.push({ kind: 'hosted-local', text: (0, localization_1.uiText)(locale, 'hostedModelsRunningInsight') });
69
+ }
70
+ else if (activeProducts.length > 1) {
66
71
  insights.push({ kind: 'workflow', text: (0, localization_1.uiText)(locale, 'insightMulti', { count: activeProducts.length }) });
67
72
  }
68
73
  else if (footprint.model_record_count > 0 && footprint.confirmed_running_model_count === 0) {
69
- insights.push({ kind: 'local-models-unused', text: (0, localization_1.uiText)(locale, 'insightModelsUnused', { count: footprint.model_record_count }) });
70
- }
71
- else if (activeProducts.length === 1 && insights[0]?.kind !== 'activity-share') {
72
- insights.push({ kind: 'workflow', text: (0, localization_1.uiText)(locale, 'insightFocused', { product: activeProducts[0].name }) });
74
+ insights.push({ kind: activeProducts.length ? 'hosted-local' : 'local-models-unused', text: (0, localization_1.uiText)(locale, activeProducts.length ? 'insightHostedFirst' : 'insightModelsUnused', { product: activeProducts[0]?.name ?? '', count: footprint.model_record_count }) });
73
75
  }
74
76
  return insights.slice(0, 3);
75
77
  }
@@ -0,0 +1,43 @@
1
+ import type { MonitorSnapshot } from './monitor';
2
+ export interface LastStoppedSummary {
3
+ saved_at: string;
4
+ observed_seconds: number;
5
+ active_seconds: number;
6
+ activity_ratio: number | null;
7
+ active_tool: string | null;
8
+ local_models_found: number;
9
+ local_models_running: number;
10
+ share_card_data?: {
11
+ ratio: number | null;
12
+ active_tool: string | null;
13
+ local_models_found: number;
14
+ local_models_running: number;
15
+ };
16
+ }
17
+ export interface LocalObservationState {
18
+ schema_version: '1.0';
19
+ current_observation_id: number;
20
+ started_at: string | null;
21
+ observed_seconds: number;
22
+ active_seconds: number;
23
+ timeline_blocks: MonitorSnapshot['timeline'];
24
+ detected_systems: Array<Pick<MonitorSnapshot['products'][number], 'signature' | 'name' | 'kind' | 'state' | 'active_seconds'>>;
25
+ app_color_mapping: Record<string, string>;
26
+ latest_insights: string[];
27
+ last_stopped_summary: LastStoppedSummary | null;
28
+ user_stopped: boolean;
29
+ snapshot: MonitorSnapshot;
30
+ }
31
+ export declare function localObservationStatePath(stateDirectory?: string): string;
32
+ export declare function loadLocalObservationState(stateDirectory?: string): Promise<LocalObservationState | null>;
33
+ export declare function saveLocalObservationState(snapshot: MonitorSnapshot, options: {
34
+ latestInsights: string[];
35
+ localModelsFound: number;
36
+ localModelsRunning: number;
37
+ appColorMapping?: Record<string, string>;
38
+ userStopped?: boolean;
39
+ lastStoppedSummary?: LastStoppedSummary | null;
40
+ stateDirectory?: string;
41
+ }): Promise<LocalObservationState>;
42
+ export declare function clearLocalObservationState(stateDirectory?: string): Promise<void>;
43
+ //# sourceMappingURL=local-observation-state.d.ts.map