attesso 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,11 +42,13 @@ export ATTESSO_SYNC_URL=https://api.attesso.com
42
42
  export ATTESSO_API_KEY=att_live_...
43
43
  npx attesso sync --dry-run
44
44
  npx attesso sync
45
+ npx attesso sync --watch
45
46
  ```
46
47
 
47
48
  The API key must belong to an entitled live workspace. A retry after a failed
48
49
  request is safe: the server deduplicates by device and sequence, and the local
49
- cursor does not move until the batch is accepted.
50
+ cursor does not move until the batch is accepted. `--watch` repeats every five
51
+ seconds, so the hosted view follows a running agent without another command.
50
52
 
51
53
  ## The overview
52
54
 
package/bin/attesso.mjs CHANGED
@@ -29,7 +29,7 @@ import { resolve } from 'node:path';
29
29
  import { CAPABILITIES, TRUST_LEVELS, validateRules } from './../src/rules.mjs';
30
30
  import { validatePathsConfig } from './../src/fs-paths.mjs';
31
31
  import { startUi } from './../src/ui/server.mjs';
32
- import { sync } from './../src/sync.mjs';
32
+ import { sync, watchSync } from './../src/sync.mjs';
33
33
 
34
34
  const argv = process.argv.slice(2);
35
35
  const dirAt = argv.indexOf('--dir');
@@ -77,8 +77,9 @@ const USAGE = `attesso: a local, signed ledger of what your agents attempted
77
77
  attesso mode [observe|enforce|off] read or set the active hook mode
78
78
  attesso paths [--json|--set FILE] read or replace the filesystem scope
79
79
  attesso ui [--port N] [--no-open] local control surface for the signed ledger
80
- attesso sync [--url URL] [--limit N] [--json] [--dry-run]
80
+ attesso sync [--url URL] [--limit N] [--json] [--dry-run] [--watch]
81
81
  send the next decisions to the hosted mirror
82
+ --watch keeps it current every 5 seconds
82
83
  (key from ATTESSO_API_KEY; never printed)
83
84
  attesso explain <seq> one entry in full: rule, tainting step, what to do
84
85
  attesso suggest [--json] rules the ledger's own counts justify
@@ -628,13 +629,30 @@ switch (command) {
628
629
  try {
629
630
  const options = flags(rest);
630
631
  const requested = options.limit === undefined ? 50 : Number(options.limit);
631
- const result = await sync({
632
- dir: DIR,
633
- url: typeof options.url === 'string' ? options.url : process.env.ATTESSO_SYNC_URL,
634
- key: typeof options.key === 'string' ? options.key : process.env.ATTESSO_API_KEY,
635
- limit: requested,
636
- dryRun: rest.includes('--dry-run'),
637
- });
632
+ const url = typeof options.url === 'string' ? options.url : process.env.ATTESSO_SYNC_URL;
633
+ const key = typeof options.key === 'string' ? options.key : process.env.ATTESSO_API_KEY;
634
+ if (rest.includes('--watch')) {
635
+ if (rest.includes('--dry-run')) throw new Error('--watch cannot be combined with --dry-run');
636
+ const controller = new AbortController();
637
+ process.on('SIGINT', () => controller.abort());
638
+ process.on('SIGTERM', () => controller.abort());
639
+ console.log('sync: watching the ledger; Ctrl-C stops');
640
+ await watchSync({
641
+ dir: DIR,
642
+ url,
643
+ key,
644
+ limit: requested,
645
+ intervalMs: options.interval === undefined ? 5000 : Number(options.interval),
646
+ signal: controller.signal,
647
+ onResult: (result) => {
648
+ if (result instanceof Error) console.error(`sync: ${result.message}`);
649
+ else if (options.json) console.log(JSON.stringify(result, null, 2));
650
+ else console.log(`sync: ${result.accepted} accepted, ${result.skipped} already present, ${result.metered} metered; last #${result.last_seq}`);
651
+ },
652
+ });
653
+ break;
654
+ }
655
+ const result = await sync({ dir: DIR, url, key, limit: requested, dryRun: rest.includes('--dry-run') });
638
656
  if (options.json) console.log(JSON.stringify(result, null, 2));
639
657
  else if (result.dry_run) console.log(`sync dry run: ${result.entries} decision(s) ready`);
640
658
  else console.log(`sync: ${result.accepted} accepted, ${result.skipped} already present, ${result.metered} metered; last #${result.last_seq}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attesso",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Bounds: decide an AI agent's next tool call before it runs, with a signed record. Free and unlimited locally, $0.00085 per synced decision.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/sync.mjs CHANGED
@@ -115,3 +115,37 @@ export async function sync({
115
115
  writeSyncState(dir, lastSeq, deviceId);
116
116
  return { accepted, skipped, metered, last_seq: lastSeq, device_id: deviceId };
117
117
  }
118
+
119
+
120
+ // watchSync keeps the hosted mirror current for a long-running agent session.
121
+ // It never advances the cursor on an error; the next tick retries the same
122
+ // window. Callers stop it with an AbortSignal.
123
+ export async function watchSync({
124
+ dir,
125
+ url,
126
+ key,
127
+ limit = DEFAULT_LIMIT,
128
+ intervalMs = 5000,
129
+ signal,
130
+ onResult = () => {},
131
+ } = {}) {
132
+ if (!Number.isInteger(intervalMs) || intervalMs < 1000 || intervalMs > 300_000) {
133
+ throw new Error('interval must be an integer from 1000 to 300000 milliseconds');
134
+ }
135
+ while (!signal?.aborted) {
136
+ try {
137
+ const result = await sync({ dir, url, key, limit });
138
+ if (result.accepted || result.skipped || result.metered) onResult(result);
139
+ } catch (error) {
140
+ onResult(error);
141
+ }
142
+ if (signal?.aborted) break;
143
+ await new Promise((resolve) => {
144
+ const timer = setTimeout(resolve, intervalMs);
145
+ signal?.addEventListener('abort', () => {
146
+ clearTimeout(timer);
147
+ resolve();
148
+ }, { once: true });
149
+ });
150
+ }
151
+ }