kira-wallet 0.2.1 → 0.2.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.
package/README.md CHANGED
@@ -15,7 +15,7 @@ web app to manage wallets, inspect token markets and follow research. Your
15
15
  existing AI agent can use the same records and actions through the Kira CLI or
16
16
  optional MCP connection, then open the web app beside your conversation.
17
17
 
18
- **This branch prepares 0.2.1.** Kira now supplies wallet tools and a full-window
18
+ **This branch prepares 0.2.2.** Kira now supplies wallet tools and a full-window
19
19
  management app. Your agent host manages its own conversations, models, files and
20
20
  approvals. Kira does not launch a model CLI or require a model login.
21
21
 
package/docs/CLI.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # CLI and provider configuration
2
2
 
3
- Kira 0.2.1 is a local wallet engine, CLI and web interface. Any capable agent
3
+ Kira 0.2.2 is a local wallet engine, CLI and web interface. Any capable agent
4
4
  can use the CLI; MCP is optional. Your agent host supplies its own model,
5
5
  conversation, general tools and approvals. Kira does not start a model process.
6
- This branch prepares 0.2.1. Registry publication remains an operator action.
6
+ This branch prepares 0.2.2. Registry publication remains an operator action.
7
7
 
8
8
  ## Install and choose a workspace
9
9
 
@@ -35,7 +35,9 @@ use the supported CLI for a consistent data root.
35
35
 
36
36
  `--json` is accepted anywhere in the command. `info` returns the version,
37
37
  data directory, capabilities, connection readiness, saved-data revision, job
38
- status, UI availability and packaged skill path. Use `--help` for syntax.
38
+ status, UI availability and packaged skill path. Its `research_connection.status`
39
+ is `ready` when Alchemy is connected and `connection_required` before that.
40
+ `--data-dir` also works before or after the subcommand. Use `--help` for syntax.
39
41
 
40
42
  ## Open the local app
41
43
 
@@ -90,7 +92,8 @@ reject the import and preserve the configuration.
90
92
  RPC, discovery and import commands apply synchronously when idle. If research
91
93
  owns the writer, they return a queued receipt; `--json` always returns a queued
92
94
  receipt without waiting. Inspect the resulting job before reporting the
93
- configuration applied. Safe configuration output contains references, not values.
95
+ configuration applied; its `result.provider_configuration` lists the applied
96
+ modes and reference names. Safe configuration output contains references, not values.
94
97
 
95
98
  New configurations allow verified public RPC fallback. `--custom-only`
96
99
  disables fallback; `--public-fallback` restores it. Existing opt-outs persist.
@@ -122,7 +125,8 @@ kira prices 'Daily wallet' --idempotency-key prices-daily-001 --json
122
125
  ```
123
126
 
124
127
  The first tag is the display name. Exact names and registered addresses select
125
- wallets. Reuse an idempotency key only when retrying the same intended request.
128
+ wallets. `add` runs the first full holdings lookup itself; use `refresh` for
129
+ later updates. Reuse an idempotency key only when retrying the same intended request.
126
130
 
127
131
  `add`, `refresh` and `prices` persist a durable job before doing research.
128
132
  `--json` or `--enqueue` returns its receipt immediately and starts a separate
@@ -175,10 +179,16 @@ kira call holdings_query --arguments '{"mode":"search","query":"ETH"}' --json
175
179
  kira call wallet_read --arguments '{"wallet":"Daily wallet","offset":0,"limit":100}' --json
176
180
  kira call token_read --arguments '{"chain_id":8453,"address":"native"}' --json
177
181
  kira call snapshots_list --arguments '{"wallet":"Daily wallet","limit":20}' --json
178
- kira call job_list --arguments '{"state":"running","offset":0,"limit":20}' --json
182
+ kira call job_list --arguments '{"state":"active","offset":0,"limit":20}' --json
179
183
  kira ui context --json
180
184
  ```
181
185
 
186
+ `job_list` accepts `state` groups `all` (default), `active` (queued or
187
+ running), `succeeded` (succeeded or partial), `attention` (interrupted or
188
+ failed) and `cancelled`, or one exact state: `queued`, `running`, `partial`,
189
+ `interrupted` or `failed`. Add `wallet` to limit results to one registered
190
+ wallet.
191
+
182
192
  Follow returned `next_offset` values when complete pages are needed. Use
183
193
  `holdings_query` for server-computed ranking, counts and aggregates across the
184
194
  full inventory. Same-symbol tokens may differ by chain and contract; retain
package/docs/RELEASE.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # Release preparation
2
2
 
3
- ## Prepared release: 0.2.1
3
+ ## Prepared release: 0.2.2
4
+
5
+ Fixes two agent contract findings from the published 0.2.1 fresh-agent test.
6
+ `job_list` now publishes its accepted `state` values in the tool schema and
7
+ accepts each exact job state, such as `running`, as well as the existing
8
+ groups. Invalid values return the accepted list. Documented `kira call`
9
+ examples are checked against the published schemas. Global `--data-dir` now
10
+ works before or after a subcommand, like `--json`.
11
+
12
+ A fresh Claude Code test of this candidate found four more contract gaps, now
13
+ fixed. `research_connection.status` is `ready` only when Alchemy is connected
14
+ and `connection_required` before that, instead of always `configured`. A
15
+ finished settings job reports the settings it applied, not the ones it
16
+ replaced. Native rows in `wallet_read` keep `address: null`, like
17
+ `holdings_query`. `snapshots_list` rows carry `phase`, so progress saved during
18
+ a running lookup is distinct from finished lookups. The skill now explains
19
+ `kira add`, import side effects, per-operation progress, price refresh
20
+ history, float position values and `head_status`.
21
+
22
+ A second, read-only Claude Code check of the fixed candidate led to three
23
+ more changes. A partial price job now names its reasons in `result.gaps`.
24
+ `kira --help` shows the `kira` program name and a line for every command.
25
+ The skill corrects `head_status`, notes the native `address: null`, the
26
+ 100-event job window and older settings receipts. Wallet records, research,
27
+ financial projections and the web app are unchanged.
28
+
29
+ Preserve the published 0.2.1 archive and every earlier archive. Freeze 0.2.2
30
+ from the accepted clean commit with `scripts/prepare-release.py`, dry-run that
31
+ exact archive and leave registry publication to the operator. Review not selected.
32
+
33
+ ## Published 0.2.1
4
34
 
5
35
  Includes the external-agent architecture described in the preserved 0.2.0
6
36
  candidate below and four approved UI refinements. Short desktop windows can
@@ -9,16 +39,10 @@ one line, a single wallet card fills its available grid width, and token detail
9
39
  captions and addresses use larger, darker text. Wallet records, research,
10
40
  financial projections, CLI and MCP behavior are unchanged.
11
41
 
12
- Preserve the already frozen 0.2.0 archive and manifest. This 0.2.1 candidate is
13
- the next publication target; an unpublished earlier candidate is not overwritten.
14
- Before freezing, run the full repository suite, clean package installation,
15
- source and history privacy checks, responsive browser acceptance including short
16
- windows and multiple wallets, and exact source/archive/installed-member checks.
17
- Freeze from the accepted clean commit, dry-run the exact archive and leave
18
- registry publication to the operator. Review not selected.
19
-
20
- The separate real-device acceptance, fresh public website screenshots and site
21
- content update remain a follow-up after installation of the new version.
42
+ The operator published 0.2.1. Its archive SHA-256 is
43
+ `aed02bc57c0fec8eec8b8fe8539ddcc45592d7876f04b86045e1d136b51804e0`; the
44
+ installed package and all 93 members matched it. The real-device acceptance
45
+ and website update that followed installation are complete.
22
46
 
23
47
  ## Preserved candidate: 0.2.0
24
48
 
@@ -135,7 +159,7 @@ The earlier unpublished 0.1.2 candidate is preserved in the ignored
135
159
  `dist/preserved-0.1.2-bad8f02/` directory. Use the current `dist/release.json`
136
160
  and its matching archive for publication; do not mix manifests from candidates.
137
161
 
138
- Keep published GitHub tags, release assets and npm versions through 0.1.20
162
+ Keep published GitHub tags, release assets and npm versions through 0.2.1
139
163
  immutable. Do not rebuild them from later documentation commits. Future package
140
164
  changes need a new version. Release preparation does not publish a GitHub
141
165
  release, promote the website or publish to npm.
@@ -151,12 +175,12 @@ python3 scripts/prepare-release.py
151
175
  ```
152
176
 
153
177
  `prepare-release.py` scans the package boundary and creates
154
- `dist/kira-wallet-0.2.1.tgz`. The ignored `dist/release.json` records its source
178
+ `dist/kira-wallet-0.2.2.tgz`. The ignored `dist/release.json` records its source
155
179
  commit, SHA-256 and member count. It never publishes. After reviewing that
156
180
  manifest, the operator's final command from the project root is:
157
181
 
158
182
  ```sh
159
- npm publish ./dist/kira-wallet-0.2.1.tgz --access public
183
+ npm publish ./dist/kira-wallet-0.2.2.tgz --access public
160
184
  ```
161
185
 
162
186
  Use the operator's npm account and complete authentication or OTP personally.
package/kira_cli.py CHANGED
@@ -186,48 +186,51 @@ def finish_configuration(store,job,*,enqueue=False):
186
186
  return 0
187
187
 
188
188
  def main():
189
- parser=argparse.ArgumentParser(description=__doc__)
189
+ parser=argparse.ArgumentParser(prog='kira',description=__doc__)
190
190
  parser.add_argument('--version',action='version',version=json.loads((ASSETS/'package.json').read_text())['version'])
191
191
  parser.add_argument('--json',action='store_true',help='Return structured output; research commands enqueue without waiting.')
192
- parser.add_argument('--data-dir',default=os.environ.get('KIRA_DATA_DIR',str(Path.home()/'.local/share/kira-wallet')))
192
+ parser.add_argument('--data-dir',default=os.environ.get('KIRA_DATA_DIR',str(Path.home()/'.local/share/kira-wallet')),help='Workspace directory. Accepted before or after the subcommand.')
193
193
  sub=parser.add_subparsers(dest='command',required=True)
194
- sub.add_parser('init');s=sub.add_parser('start');s.add_argument('--port',type=int,default=8787)
194
+ sub.add_parser('init',help='Create or check the data directory without changing records.');s=sub.add_parser('start',help='Open the local app.');s.add_argument('--port',type=int,default=8787)
195
195
  mode=s.add_mutually_exclusive_group()
196
196
  mode.add_argument('--read-only',dest='controls',action='store_false',help='View recorded holdings without local browser actions.')
197
197
  mode.add_argument('--controls',dest='controls',action='store_true',help=argparse.SUPPRESS)
198
198
  s.set_defaults(controls=True)
199
199
  s.add_argument('--no-open',action='store_true',help='Print the local app URL without opening a browser.')
200
- setup=sub.add_parser('setup');setup.add_argument('--port',type=int,default=8787);setup.add_argument('--no-open',action='store_true')
201
- sub.add_parser('stop');sub.add_parser('status');sub.add_parser('info')
202
- wallets=sub.add_parser('wallets');walletsub=wallets.add_subparsers(dest='wallet_action')
200
+ setup=sub.add_parser('setup',help='Open the local app at first-run setup.');setup.add_argument('--port',type=int,default=8787);setup.add_argument('--no-open',action='store_true')
201
+ sub.add_parser('stop',help='Stop the local app.');sub.add_parser('status',help='Show whether the local app is running.');sub.add_parser('info',help='Show version, data directory, readiness and active jobs.')
202
+ wallets=sub.add_parser('wallets',help='List wallets or change their names.');walletsub=wallets.add_subparsers(dest='wallet_action')
203
203
  tags=walletsub.add_parser('tags');tags.add_argument('wallet');tags.add_argument('--tag',action='append',required=True);tags.add_argument('--idempotency-key')
204
- tools=sub.add_parser('tools');tools.add_argument('--allow-writes',action='store_true')
205
- call=sub.add_parser('call');call.add_argument('tool');call.add_argument('--arguments',default='{}')
206
- sub.add_parser('skill')
207
- ui=sub.add_parser('ui');ui.add_argument('action',choices=['context'])
208
- archive=sub.add_parser('archive');archive.add_argument('action',choices=['list','read','workspaces','files','export']);archive.add_argument('conversation',nargs='?');archive.add_argument('--path');archive.add_argument('--output')
209
- connection=sub.add_parser('connect');connection.add_argument('provider',choices=['alchemy'])
210
- d=sub.add_parser('doctor');d.add_argument('--live',action='store_true');d.add_argument('--chains',default='1,8453,81457');d.add_argument('--budget',type=int,default=30)
211
- demo=sub.add_parser('demo');demo.add_argument('--port',type=int,default=8790)
212
- add=sub.add_parser('add');add.add_argument('address');add.add_argument('--tag',required=True);add.add_argument('--resume')
213
- refresh=sub.add_parser('refresh');refresh.add_argument('wallet');refresh.add_argument('--resume')
214
- prices=sub.add_parser('prices');prices.add_argument('wallet')
204
+ tools=sub.add_parser('tools',help='Serve Kira tools over MCP stdio.');tools.add_argument('--allow-writes',action='store_true')
205
+ call=sub.add_parser('call',help='Run one Kira tool with JSON arguments.');call.add_argument('tool');call.add_argument('--arguments',default='{}')
206
+ sub.add_parser('skill',help='Print the agent instructions.')
207
+ ui=sub.add_parser('ui',help="Read the app's last recorded selection.");ui.add_argument('action',choices=['context'])
208
+ archive=sub.add_parser('archive',help='Read earlier conversations and generated files.');archive.add_argument('action',choices=['list','read','workspaces','files','export']);archive.add_argument('conversation',nargs='?');archive.add_argument('--path');archive.add_argument('--output')
209
+ connection=sub.add_parser('connect',help='Enter an Alchemy key at a hidden local prompt.');connection.add_argument('provider',choices=['alchemy'])
210
+ d=sub.add_parser('doctor',help='Check RPC settings, optionally with live probes.');d.add_argument('--live',action='store_true');d.add_argument('--chains',default='1,8453,81457');d.add_argument('--budget',type=int,default=30)
211
+ demo=sub.add_parser('demo',help='Open a sample portfolio.');demo.add_argument('--port',type=int,default=8790)
212
+ add=sub.add_parser('add',help='Register a public wallet and run its first holdings lookup.');add.add_argument('address');add.add_argument('--tag',required=True);add.add_argument('--resume')
213
+ refresh=sub.add_parser('refresh',help='Run a new holdings lookup for a wallet.');refresh.add_argument('wallet');refresh.add_argument('--resume')
214
+ prices=sub.add_parser('prices',help='Refresh prices without changing balances.');prices.add_argument('wallet')
215
215
  for mutation in (add,refresh,prices):mutation.add_argument('--idempotency-key');mutation.add_argument('--enqueue',action='store_true')
216
- jobs=sub.add_parser('jobs');jobsub=jobs.add_subparsers(dest='action',required=True)
216
+ jobs=sub.add_parser('jobs',help='List, follow and control durable jobs.');jobsub=jobs.add_subparsers(dest='action',required=True)
217
217
  jobsub.add_parser('list');jobsub.add_parser('run')
218
218
  wait=jobsub.add_parser('wait');wait.add_argument('job_id');wait.add_argument('--timeout',type=float,default=30)
219
219
  for action in ('read','resume','cancel','deepen'):
220
220
  item=jobsub.add_parser(action);item.add_argument('job_id');item.add_argument('--idempotency-key')
221
- config=sub.add_parser('config');configsub=config.add_subparsers(dest='setting',required=True)
221
+ config=sub.add_parser('config',help='Show settings or import a key file reference.');configsub=config.add_subparsers(dest='setting',required=True)
222
222
  configsub.add_parser('show')
223
223
  imp=configsub.add_parser('import-env');imp.add_argument('--file',required=True);imp.add_argument('--key-env',default='ALCHEMY_API_KEY')
224
- rpc=sub.add_parser('rpc');rpcsub=rpc.add_subparsers(dest='setting',required=True)
224
+ rpc=sub.add_parser('rpc',help='Choose public or custom RPC references.');rpcsub=rpc.add_subparsers(dest='setting',required=True)
225
225
  rpcsub.add_parser('public')
226
226
  custom=rpcsub.add_parser('set');custom.add_argument('--chain',type=int,required=True);custom.add_argument('--url-env',required=True);custom.add_argument('--priority',choices=['public_first','custom_first']);fallback=custom.add_mutually_exclusive_group();fallback.add_argument('--public-fallback',action='store_true',default=None);fallback.add_argument('--custom-only',dest='public_fallback',action='store_false')
227
- discovery=sub.add_parser('discovery');discovery.add_argument('provider',choices=['none','alchemy']);discovery.add_argument('--key-env',default='ALCHEMY_API_KEY');discovery.add_argument('--explorers',action='store_true')
227
+ discovery=sub.add_parser('discovery',help='Choose the token discovery provider.');discovery.add_argument('provider',choices=['none','alchemy']);discovery.add_argument('--key-env',default='ALCHEMY_API_KEY');discovery.add_argument('--explorers',action='store_true')
228
228
  argv=sys.argv[1:]
229
- # A global machine-output switch works before or after a subcommand.
229
+ # Global switches work before or after a subcommand.
230
230
  if '--json' in argv:argv=['--json']+[a for a in argv if a!='--json']
231
+ for index,item in enumerate(argv):
232
+ if item=='--data-dir' and index+1<len(argv):argv=argv[index:index+2]+argv[:index]+argv[index+2:];break
233
+ if item.startswith('--data-dir='):argv=[item]+argv[:index]+argv[index+1:];break
231
234
  args=parser.parse_args(argv);root=Path(args.data_dir).expanduser().resolve();os.environ['KIRA_DATA_DIR']=str(root)
232
235
  if sys.version_info<(3,11):raise ValueError('Python 3.11 or newer is required.')
233
236
  if os.name!='posix':raise ValueError('This development build requires a POSIX host. macOS is verified.')
package/kira_config.py CHANGED
@@ -82,9 +82,10 @@ def public_status(root=None):
82
82
  rpc=cfg['rpc'];discovery=cfg['discovery']
83
83
  public=rpc['mode']=='public' or rpc['allow_public_fallback']
84
84
  import_command=f"kira config import-env --file PATH --key-env {discovery['key_env']}"
85
- return {'status':'configured','rpc_mode':rpc['mode'],'read_order':('custom_first' if discovery['provider']=='alchemy' else rpc.get('priority','public_first')) if public else 'custom_only',
85
+ ready=discovery['provider']=='alchemy' and available
86
+ return {'status':'ready' if ready else 'connection_required','rpc_mode':rpc['mode'],'read_order':('custom_first' if discovery['provider']=='alchemy' else rpc.get('priority','public_first')) if public else 'custom_only',
86
87
  'custom_chain_count':len(rpc['chains']) if rpc['mode']=='custom' else 0,
87
- 'discovery_provider':discovery['provider'],'discovery_key_available':discovery['provider']=='alchemy' and available,
88
+ 'discovery_provider':discovery['provider'],'discovery_key_available':ready,
88
89
  'key_env':discovery['key_env'],'public_discovery':False,'required_provider':'alchemy',
89
90
  'import_command':import_command,
90
91
  'guide':'Open Workspace settings > Alchemy. Run kira connect alchemy in a terminal and enter the app key at its hidden prompt. Return to Kira to check the connection, then refresh holdings. Actual results establish network access. Never enter or request a key in chat or the browser.',
package/kira_jobs.py CHANGED
@@ -35,6 +35,10 @@ def signal_process_group(pgid, sig):
35
35
  State = Literal['queued', 'running', 'interrupted', 'succeeded', 'partial', 'failed', 'cancelled']
36
36
  Operation = Literal['wallet.add','wallet.refresh','prices.refresh','wallet.setTags','settings.rpc','settings.discovery','settings.provider','settings.importEnv']
37
37
  TERMINAL = {'succeeded', 'partial', 'failed', 'cancelled', 'interrupted'}
38
+ # Web history uses the groups. Agents may also filter by an exact state they read on a job.
39
+ STATE_FILTERS = {'all': None, 'active': {'queued','running'}, 'succeeded': {'succeeded','partial'},
40
+ 'attention': {'interrupted','failed'}, 'cancelled': {'cancelled'},
41
+ **{state: {state} for state in ('queued','running','partial','interrupted','failed')}}
38
42
  ADDRESS = re.compile(r'0x[0-9a-fA-F]{40}')
39
43
 
40
44
  class Request(TypedDict):
@@ -133,6 +137,11 @@ def fields(value, required):
133
137
  if not isinstance(value, dict) or set(value) != set(required):
134
138
  raise JobError('invalid_input', 'Unexpected or missing operation fields.')
135
139
 
140
+ def provider_summary(config):
141
+ """Settings facts and environment names only, never values."""
142
+ return {'rpc_mode':config['rpc']['mode'],'discovery_provider':config['discovery']['provider'],
143
+ 'public_fallback':config['rpc']['allow_public_fallback'],'endpoint_refs':{cid:row['url_env'] for cid,row in config['rpc']['chains'].items()}}
144
+
136
145
  def identifier(value):
137
146
  if not isinstance(value, str): raise JobError('invalid_id', 'Expected a job ID.')
138
147
  try:
@@ -238,12 +247,10 @@ class JobStore:
238
247
  def page(self, offset=0, limit=50, state='all', wallet=None):
239
248
  from kira_state import page_bounds
240
249
  page_bounds(offset, limit)
241
- groups = {'all': None, 'active': {'queued','running'}, 'succeeded': {'succeeded','partial'},
242
- 'attention': {'interrupted','failed'}, 'cancelled': {'cancelled'}}
243
- if state not in groups:raise JobError('invalid_state','Choose a supported job state filter.')
250
+ if state not in STATE_FILTERS:raise JobError('invalid_state','Choose a job state filter: '+', '.join(STATE_FILTERS)+'.')
244
251
  selected = self.wallet(wallet)['address_key'] if wallet else None
245
252
  with self.locked():
246
- rows = [r for r in self.summaries() if (groups[state] is None or r['state'] in groups[state])
253
+ rows = [r for r in self.summaries() if (STATE_FILTERS[state] is None or r['state'] in STATE_FILTERS[state])
247
254
  and (selected is None or (r['input'].get('wallet') or r['input'].get('address','')).lower() == selected)]
248
255
  rows.sort(key=lambda r: r['created_at'], reverse=True)
249
256
  jobs = [self.get(r['job_id']) for r in rows[offset:offset+limit]]
@@ -545,7 +552,13 @@ class JobStore:
545
552
  paths = {w['latest_snapshot']['directory'] for w in read(self.root / 'wallets.json')['wallets'] if w['address_key'] == key and w.get('latest_snapshot')}
546
553
  for run in read(self.root / 'wallets.json').get('research_runs', []):
547
554
  if run['address_key'] == key: paths.add(run['snapshot'])
548
- return sorted([{'snapshot_id': p, 'compiled_at': self.snapshot(p).get('compiled_at'), 'status': self.snapshot(p).get('status')} for p in paths],key=lambda r:r['compiled_at'] or '')
555
+ rows = []
556
+ for path in paths:
557
+ # phase "initial" marks progress published while a holdings lookup was still running.
558
+ snapshot = self.snapshot(path)
559
+ rows.append({'snapshot_id': path, 'compiled_at': snapshot.get('compiled_at'), 'status': snapshot.get('status'),
560
+ 'phase': (snapshot.get('pipeline') or {}).get('phase')})
561
+ return sorted(rows, key=lambda r: r['compiled_at'] or '')
549
562
 
550
563
  def publication(self, job):
551
564
  if job['operation'].startswith('settings.') or job['operation']=='wallet.setTags':
@@ -568,7 +581,8 @@ class JobStore:
568
581
  previous = read(projection) if projection.exists() else {}
569
582
  value = read(overlay)
570
583
  if (previous.get('observed_at') or '') <= value['observed_at']: atomic(projection, value)
571
- return {'snapshot_id': item['snapshot_id'], 'overlay': item['overlay'], 'status': item['status']}
584
+ return {'snapshot_id': item['snapshot_id'], 'overlay': item['overlay'], 'status': item['status'],
585
+ **({'gaps': value['job_result_gaps']} if isinstance(value.get('job_result_gaps'), list) else {})}
572
586
  manifest = folder / 'run.json'
573
587
  if not manifest.exists() or not (folder / 'results.json').exists(): return None
574
588
  result = read(folder / 'results.json')
@@ -710,7 +724,8 @@ class JobStore:
710
724
  if job['input']['provider']=='alchemy' and not secret_values(config).get(job['input']['key_env']):
711
725
  raise JobError('connection_key_missing','Kira cannot find the local Alchemy key. Follow the connection guide, then retry. Existing settings are unchanged.')
712
726
  atomic(config_path(),config)
713
- result={'status':'completed','setting':job['operation']};atomic(self.jobs/'results'/(job['job_id']+'.json'),result);return result
727
+ result={'status':'completed','setting':job['operation'],'provider_configuration':provider_summary(config)}
728
+ atomic(self.jobs/'results'/(job['job_id']+'.json'),result);return result
714
729
  self.require_connection()
715
730
  env = {**os.environ, 'KIRA_DATA_DIR': str(self.root), 'KIRA_JOB_ID': job['job_id'], 'KIRA_JOB_SNAPSHOT': job['checkpoint'], 'KIRA_ANALYSIS_FD': str(writer.fileno()), 'PYTHONUNBUFFERED': '1', 'KIRA_RESEARCH_GENERATION':job.get('parent_job_id',job['job_id'])}
716
731
  command=self.engine_command(job)
@@ -815,10 +830,10 @@ class JobStore:
815
830
  if entry.get('research_generation') not in (None,job.get('parent_job_id')):
816
831
  job.update(state='cancelled',stage='stopped',errors=[{'code':'superseded','message':'Newer holdings research replaced this detailed run. Earlier evidence is preserved.'}]);self.save(job);writer.close();continue
817
832
  if job:
818
- from kira_config import load_config
819
- config=load_config()
820
- job['provider_configuration']={'rpc_mode':config['rpc']['mode'],'discovery_provider':config['discovery']['provider'],
821
- 'public_fallback':config['rpc']['allow_public_fallback'],'endpoint_refs':{cid:row['url_env'] for cid,row in config['rpc']['chains'].items()}}
833
+ # Research records the settings it ran with. Settings jobs report their result instead.
834
+ if not job['operation'].startswith('settings.'):
835
+ from kira_config import load_config
836
+ job['provider_configuration']=provider_summary(load_config())
822
837
  job.update(state='running', stage='starting', started_at=now(), attempt=job['attempt'] + 1, errors=[]); self.save(job)
823
838
  if not job:
824
839
  time.sleep(.5)
package/kira_records.py CHANGED
@@ -209,7 +209,7 @@ def projection(root, config, *, token_id=None, limit=12, offset=0, wallet_key=No
209
209
  def compact_wallet_facts(rows):
210
210
  asset_fields=('id','chain_id','address','display_identity','symbol','name','environment','balance','is_native','value_usd','historical_value_usd','balance_observed_at','price','pools','pools_recorded','pools_omitted','exit_quote_recorded','archived')
211
211
  return [{**row,'assets':[{key:asset[key] for key in asset_fields if key in asset and
212
- (asset[key] is not None or key in ('value_usd','price'))}
212
+ (asset[key] is not None or key in ('address','value_usd','price'))}
213
213
  for asset in row['assets']]} for row in rows]
214
214
 
215
215
  def recorded_valuation(assets):
package/kira_service.py CHANGED
@@ -4,7 +4,7 @@ import json
4
4
  from pathlib import Path
5
5
  import time
6
6
  import uuid
7
- from kira_jobs import JobStore, JobError, atomic, fields
7
+ from kira_jobs import JobStore, JobError, atomic, fields, STATE_FILTERS
8
8
  from kira_state import StateCache, page, page_bounds
9
9
  from kira_research_tools import ResearchTools, CATALOG
10
10
 
@@ -21,8 +21,12 @@ for row in CATALOG:
21
21
  properties[name]={**schema,'description':description}
22
22
  if row['name'] in ('portfolio_read','job_list','snapshots_list'):
23
23
  properties.update(offset=INTEGER,limit={'type':'integer','minimum':1,'maximum':100})
24
- if row['name']=='job_list':properties.update(state=STRING,wallet=STRING)
25
- READ_TOOLS.append({'name':row['name'],'description':row['description'],
24
+ summary=row['description']
25
+ if row['name']=='job_list':
26
+ summary='Read a newest-first page of durable jobs. Optionally filter by state or registered wallet.'
27
+ properties.update(state={'type':'string','enum':list(STATE_FILTERS),'description':'Default all. Groups: active (queued, running), succeeded (succeeded, partial), attention (interrupted, failed). The other values match one exact state.'},
28
+ wallet={**STRING,'description':'registered address or exact name'})
29
+ READ_TOOLS.append({'name':row['name'],'description':summary,
26
30
  'inputSchema':{'type':'object','properties':properties,'required':list(row['arguments']),'additionalProperties':False},
27
31
  'annotations':{'readOnlyHint':True,'destructiveHint':False,'idempotentHint':True,'openWorldHint':False}})
28
32
 
@@ -63,6 +67,8 @@ def validate(arguments,schema):
63
67
  field=schema['properties'][name];types=field['type'] if isinstance(field['type'],list) else [field['type']]
64
68
  if not any((type(value) in kinds[k] if isinstance(kinds[k],tuple) else type(value) is kinds[k]) for k in types):
65
69
  raise JobError('invalid_input','Invalid argument type: '+name)
70
+ if 'enum' in field and value not in field['enum']:
71
+ raise JobError('invalid_input','Choose '+name+' from: '+', '.join(map(str,field['enum']))+'.')
66
72
  if isinstance(value,str) and (len(value)>field.get('maxLength',500) or len(value)<field.get('minLength',0)):
67
73
  raise JobError('invalid_input','Use a bounded identifier: '+name)
68
74
  if type(value) in (int,float) and (value<field.get('minimum',value) or value>field.get('maximum',value)):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kira-wallet",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Local wallet research for your agent session and a human-controlled web workspace.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -12,8 +12,9 @@ and MCP operations.
12
12
 
13
13
  ## Start with the existing workspace
14
14
 
15
- 1. Run `kira --version`, then `kira info --json` and inspect readiness and
16
- `data_dir`. The commands below require 0.2.0 or later. If installation is
15
+ 1. Run `kira --version`, then `kira info --json` and inspect `data_dir` and
16
+ `research_connection.status`: `ready` means Alchemy is connected;
17
+ `connection_required` means new research cannot start yet. The commands below require 0.2.0 or later. If installation is
17
18
  part of the request, use `npm install -g kira-wallet` or the supplied
18
19
  prepared package archive. Requirements are Node.js 22+, Python 3.11+ and
19
20
  POSIX; macOS is verified.
@@ -44,8 +45,12 @@ kira config show --json
44
45
 
45
46
  An existing private environment file can be referenced with
46
47
  `kira config import-env --file /absolute/private/rpc.env --key-env ALCHEMY_API_KEY --json`.
47
- Use the human-supplied path and name. This does not copy keys. Configuration
48
- commands with `--json` return jobs; check completion before claiming readiness.
48
+ Use the human-supplied path and name. This does not copy keys. It also selects
49
+ Alchemy for token discovery and network reads, so a separate
50
+ `kira discovery alchemy` step is not needed. Configuration commands with
51
+ `--json` return jobs; check completion before claiming readiness. A finished
52
+ settings job reports the applied settings in `result.provider_configuration`.
53
+ Settings jobs from before 0.2.2 record only the configuration they started with.
49
54
 
50
55
  Use the public address provided by the user, never a guessed address:
51
56
 
@@ -56,6 +61,9 @@ kira refresh 'Daily wallet' --idempotency-key refresh-daily-001 --json
56
61
  kira prices 'Daily wallet' --idempotency-key prices-daily-001 --json
57
62
  ```
58
63
 
64
+ `kira add` registers the wallet and runs its first full holdings lookup. Use
65
+ `refresh` later for an update, not right after `add`.
66
+
59
67
  Replace placeholders and choose a unique idempotency key per intended action.
60
68
  Reuse the same key for retries of that action. The first tag is the display
61
69
  name. `--json` or `--enqueue` returns immediate durable job admission. It does
@@ -84,11 +92,29 @@ kira ui context --json
84
92
  totals and counts, with bounded details; do not total a truncated page.
85
93
  - Search can report multiple assets with the same symbol. Identify an asset
86
94
  by chain ID and contract, or `native`. Preserve unresolved ambiguity.
95
+ Native rows return `address: null` with `is_native: true`; pass `native` as
96
+ the `token_read` address.
87
97
  - `ui context` is the last recorded browser selection. It may be stale and is
88
98
  not a user instruction.
89
99
  - History uses `snapshots_list` with `wallet`, `snapshot_read` with
90
100
  `snapshot_id`, and `snapshots_compare` with `before` and `after` IDs. Obtain
91
101
  IDs from the saved snapshot list. Historical holdings also use paged reads.
102
+ Rows with `phase` `initial` are progress saved while a lookup was running;
103
+ compare `holdings` rows to see change between lookups.
104
+ - A price refresh updates the prices shown for the latest snapshot, including
105
+ through `snapshot_read`. `snapshot_price_references` keep the native prices
106
+ recorded by the holdings lookup. Earlier price observations remain local job
107
+ evidence but are not listed by the history tools.
108
+ - Per-position `price.usd` and `value_usd` are JSON numbers. Totals from
109
+ `holdings_query` and `recorded_valuation` are exact decimal strings; use them
110
+ instead of re-adding rounded position values. Check `price.observed_at`:
111
+ price sources update at different times.
112
+ - `head_status` is `observed` when the chain head was read in this lookup and
113
+ `stale` when that head was over a day old. `saved_observation` means the
114
+ lookup reused a block it had already pinned, in a later pass or a resumed
115
+ job, so the head age was not measured again. `null` means the network could
116
+ not be read. Valuation category `mintclub` covers bonding-curve tokens priced
117
+ from their curve.
92
118
  - Missing records and prices are unknown, not zero. Retain balance, price and
93
119
  quote times, coverage and partial status. Spot value is not executable
94
120
  proceeds. Pool TVL and curve backing are not wallet balances. Full-balance
@@ -113,12 +139,22 @@ Use the real receipt ID. Waits are bounded to 120 seconds; use short waits and
113
139
  keep the user informed. `completed` only means the job left queued/running.
114
140
  Inspect `job.state`, result, errors and coverage before reporting success.
115
141
  Partial, interrupted, cancelled and failed states have different meanings.
142
+ Progress events count work per `context.operation`, such as `registry` or
143
+ `balances`. Balance totals grow as the registry scan finds candidates, so do
144
+ not derive one overall percentage. A job's `chains` entries report network
145
+ reads; use `wallet_read` or `network_read` coverage for discovery gaps. A
146
+ partial price job names its reasons in `result.gaps`. Job records keep the
147
+ latest 100 events.
116
148
  Accepted work survives viewer shutdown or agent disconnection. Cancel only
117
149
  when requested; resume interrupted jobs explicitly. `jobs deepen` adds
118
150
  optional sell-route analysis from saved holdings without rediscovering tokens.
119
151
 
120
152
  Use `call job_list` with optional `state`, `wallet`, `offset` and `limit` for
121
- paged job discovery. `kira jobs run` starts queued work, not interrupted jobs.
153
+ paged job discovery. `state` is `all` (default), a group (`active`,
154
+ `succeeded`, `attention`, `cancelled`) or an exact state (`queued`, `running`,
155
+ `partial`, `interrupted`, `failed`). `succeeded` includes partial results;
156
+ check `job.state` to separate them.
157
+ `kira jobs run` starts queued work, not interrupted jobs.
122
158
 
123
159
  ## Shared tools and earlier archives
124
160
 
@@ -162,7 +162,15 @@ for w in selected:
162
162
  overlay=job_folder/'prices.json'
163
163
  output['snapshot_id']=w['latest_snapshot']['directory']
164
164
  missing_native=any(c.get('environment')=='mainnet' and float(c.get('native_balance') or 0)>0 and c.get('native_symbol')+'_USD' not in fresh_native for c in snapshot.get('coverage',[]))
165
- output['job_result_status']='completed_with_coverage_gaps' if output.get('market_discovery_status')=='partial' or missing_native or retained or output.get('reserve_price_status')=='partial' or (output.get('reserve_price_graph') or {}).get('errors') or rpc.get('errors') or any('error' in p for p in rpc['tokens']) or any(isinstance(r['response'],dict) and 'transport_error' in r['response'] for r in fetched) else 'completed'
165
+ # Name each gap so a partial price job explains itself.
166
+ output['job_result_gaps']=[name for name,found in (
167
+ ('market_discovery_partial',output.get('market_discovery_status')=='partial'),
168
+ ('native_price_missing',missing_native),
169
+ ('earlier_prices_retained',bool(retained)),
170
+ ('reserve_pricing_partial',output.get('reserve_price_status')=='partial' or bool((output.get('reserve_price_graph') or {}).get('errors'))),
171
+ ('curve_reads_incomplete',bool(rpc.get('errors')) or any('error' in p for p in rpc['tokens'])),
172
+ ('provider_request_failed',any(isinstance(r['response'],dict) and 'transport_error' in r['response'] for r in fetched))) if found]
173
+ output['job_result_status']='completed_with_coverage_gaps' if output['job_result_gaps'] else 'completed'
166
174
  atomic(overlay,output)
167
175
  atomic(job_folder/'price-publication.json',{'schema_version':1,'snapshot_id':w['latest_snapshot']['directory'],
168
176
  'overlay':str(overlay.relative_to(h.ROOT)),'projection':str(destination.relative_to(h.ROOT)),