sentisense 0.43.0 → 0.45.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
@@ -88,6 +88,25 @@ something supplementary does not come back, such as the Score history behind a s
88
88
  command still prints its answer and exits 0 with a `note:` line on stderr, so stdout stays
89
89
  clean for a pipe and the gap is never silent.
90
90
 
91
+ ### Saying who is calling
92
+
93
+ If you set `SENTISENSE_AGENT_NAME` (what your agent is called) and `SENTISENSE_SKILL` (the
94
+ slug of the skill driving it), requests carry that identity, so usage can be understood and
95
+ the tools improved. Both are optional, never required, and nothing is inferred when they are
96
+ absent.
97
+
98
+ ```bash
99
+ export SENTISENSE_AGENT_NAME=research-desk
100
+ export SENTISENSE_SKILL=stock-analysis
101
+ npx -y sentisense@latest quote NVDA
102
+ # User-Agent: sentisense-node/0.44.0 sentisense-cli/0.44.0 (stock-analysis; agent/research-desk)
103
+ ```
104
+
105
+ Either can also be a flag (`--agent`, `--skill`) or a stored setting
106
+ (`sentisense auth --agent research-desk --skill stock-analysis`), resolved flag first, then
107
+ environment, then config. Values are reduced to letters, digits, dot, underscore and hyphen,
108
+ and capped at 32 characters, so nothing you set can reshape the header.
109
+
91
110
  ### Exit codes
92
111
 
93
112
  Failures print two lines to stderr, what went wrong and what to do about it, and exit with a
package/dist/cli.cjs CHANGED
@@ -55,8 +55,8 @@ var EXIT = {
55
55
  };
56
56
  var EXIT_TABLE = [
57
57
  [EXIT.OK, "success"],
58
- [EXIT.ERROR, "API error or unexpected failure"],
59
- [EXIT.USAGE, "bad usage: unknown command, flag, or missing argument"],
58
+ [EXIT.ERROR, "API error, including a request the API rejected as invalid"],
59
+ [EXIT.USAGE, "bad usage, caught before any request was sent"],
60
60
  [EXIT.AUTH, "missing or rejected API key"],
61
61
  [EXIT.NOT_FOUND, "no data for that symbol or identifier"],
62
62
  [EXIT.RATE_LIMIT, "rate limited"],
@@ -83,14 +83,14 @@ var MissingKeyError = class extends Error {
83
83
  this.name = "MissingKeyError";
84
84
  }
85
85
  };
86
- function describeError(error, debug) {
87
- const report = classify(error);
86
+ function describeError(error, debug, command) {
87
+ const report = classify(error, command);
88
88
  if (debug && error instanceof Error && error.stack) {
89
89
  report.lines.push(error.stack);
90
90
  }
91
91
  return report;
92
92
  }
93
- function classify(error) {
93
+ function classify(error, command) {
94
94
  if (error instanceof MissingKeyError) {
95
95
  return {
96
96
  exitCode: EXIT.AUTH,
@@ -156,6 +156,16 @@ function classify(error) {
156
156
  ]
157
157
  };
158
158
  }
159
+ if (error.status !== void 0 && error.status >= 400 && error.status < 500) {
160
+ const help = command ? `sentisense help ${command}` : "sentisense --help";
161
+ return {
162
+ exitCode: EXIT.ERROR,
163
+ lines: [
164
+ `error: the API rejected the request (${error.status}): ${error.message}`,
165
+ `next: check the flags and values you passed. Run "${help}" for the accepted fields and examples.`
166
+ ]
167
+ };
168
+ }
159
169
  return {
160
170
  exitCode: EXIT.ERROR,
161
171
  lines: [
@@ -268,6 +278,16 @@ function signedPercent(value, decimals = 2) {
268
278
  function percent(value, decimals = 1) {
269
279
  return isNum(value) ? `${value.toFixed(decimals)}%` : ABSENT;
270
280
  }
281
+ var MAX_RATIO_DECIMALS = 6;
282
+ function ratioPercent(value, decimals = 2) {
283
+ if (!isNum(value)) return ABSENT;
284
+ const scaled = value * 100;
285
+ let places = decimals;
286
+ while (scaled !== 0 && places < MAX_RATIO_DECIMALS && Number(scaled.toFixed(places)) === 0) {
287
+ places += 1;
288
+ }
289
+ return `${scaled.toFixed(places)}%`;
290
+ }
271
291
  function money(value, decimals = 2) {
272
292
  return isNum(value) ? `$${value.toFixed(decimals)}` : ABSENT;
273
293
  }
@@ -435,6 +455,7 @@ function readConfig(dir) {
435
455
  const config = {};
436
456
  if (typeof raw.apiKey === "string") config.apiKey = raw.apiKey;
437
457
  if (typeof raw.agentName === "string") config.agentName = raw.agentName;
458
+ if (typeof raw.skill === "string") config.skill = raw.skill;
438
459
  if (typeof raw.baseUrl === "string") config.baseUrl = raw.baseUrl;
439
460
  return config;
440
461
  } catch {
@@ -464,10 +485,11 @@ function maskKey(key) {
464
485
  var authCommand = {
465
486
  name: "auth",
466
487
  summary: "Store an API key, show what is configured, or remove it",
467
- usage: "sentisense auth [<key>] [--agent <name>] [--remove]",
488
+ usage: "sentisense auth [<key>] [--agent <name>] [--skill <slug>] [--remove]",
468
489
  examples: [
469
490
  "sentisense auth $SENTISENSE_API_KEY",
470
491
  "sentisense auth --agent research-desk",
492
+ "sentisense auth --skill stock-analysis --agent research-desk",
471
493
  "sentisense auth",
472
494
  "sentisense auth --remove"
473
495
  ],
@@ -475,8 +497,10 @@ var authCommand = {
475
497
  "Settings live in config.json under $SENTISENSE_CONFIG_DIR, $XDG_CONFIG_HOME/sentisense,",
476
498
  "or ~/.config/sentisense, written owner-readable only (0600).",
477
499
  "The key is never printed back in full, and never has to be pasted into a command again.",
478
- "An agent name is optional. When set, it rides along in the User-Agent so your own",
479
- "traffic is easy to tell apart from everything else calling the API."
500
+ "Two optional labels say who is calling: --agent is what your agent calls itself, and",
501
+ "--skill is the slug of the skill driving it. When set, they ride along in the",
502
+ "User-Agent, so usage can be understood and the tools improved. Both are voluntary and",
503
+ "nothing needs them to work."
480
504
  ],
481
505
  flags: {
482
506
  remove: { type: "boolean", describe: "Delete the stored settings" }
@@ -495,13 +519,15 @@ var authCommand = {
495
519
  }
496
520
  const key = args.positionals[0];
497
521
  const agent = typeof args.flags.agent === "string" ? args.flags.agent : void 0;
522
+ const skill = typeof args.flags.skill === "string" ? args.flags.skill : void 0;
498
523
  const baseUrl = typeof args.flags["base-url"] === "string" ? args.flags["base-url"] : void 0;
499
- if (key || agent || baseUrl) {
524
+ if (key || agent || skill || baseUrl) {
500
525
  const stored2 = readConfig(dir);
501
526
  const next = {
502
527
  ...stored2,
503
528
  ...key ? { apiKey: key } : {},
504
529
  ...agent ? { agentName: agent } : {},
530
+ ...skill ? { skill } : {},
505
531
  ...baseUrl ? { baseUrl } : {}
506
532
  };
507
533
  writeConfig(dir, next);
@@ -510,6 +536,7 @@ var authCommand = {
510
536
  path,
511
537
  apiKey: next.apiKey ? maskKey(next.apiKey) : null,
512
538
  agentName: next.agentName ?? null,
539
+ skill: next.skill ?? null,
513
540
  baseUrl: next.baseUrl ?? null
514
541
  },
515
542
  doc: doc(
@@ -519,6 +546,7 @@ var authCommand = {
519
546
  items: fields(
520
547
  next.apiKey ? field("api key", maskKey(next.apiKey)) : void 0,
521
548
  next.agentName ? field("agent", next.agentName) : void 0,
549
+ next.skill ? field("skill", next.skill) : void 0,
522
550
  next.baseUrl ? field("base url", next.baseUrl) : void 0
523
551
  )
524
552
  }
@@ -534,6 +562,7 @@ var authCommand = {
534
562
  apiKey: resolved ? maskKey(resolved) : null,
535
563
  apiKeySource: context.apiKeySource,
536
564
  agentName: context.agentName ?? null,
565
+ skill: context.skill ?? null,
537
566
  baseUrl: context.baseUrl ?? null
538
567
  },
539
568
  doc: doc(
@@ -544,6 +573,7 @@ var authCommand = {
544
573
  field("source", resolved ? context.apiKeySource : "none"),
545
574
  field("config", path),
546
575
  context.agentName ? field("agent", context.agentName) : void 0,
576
+ context.skill ? field("skill", context.skill) : void 0,
547
577
  context.baseUrl ? field("base url", context.baseUrl) : void 0
548
578
  )
549
579
  },
@@ -1008,7 +1038,7 @@ var flowsCommand = {
1008
1038
  };
1009
1039
 
1010
1040
  // src/version.ts
1011
- var VERSION = "0.43.0";
1041
+ var VERSION = "0.45.0";
1012
1042
 
1013
1043
  // src/resources/analyst.ts
1014
1044
  var Analyst = class {
@@ -2190,7 +2220,12 @@ var GLOBAL_FLAGS = {
2190
2220
  agent: {
2191
2221
  type: "string",
2192
2222
  placeholder: "name",
2193
- describe: "Label this call in the User-Agent"
2223
+ describe: "Name your agent in the User-Agent, if you want to"
2224
+ },
2225
+ skill: {
2226
+ type: "string",
2227
+ placeholder: "slug",
2228
+ describe: "Name the skill driving this call, if you want to"
2194
2229
  }
2195
2230
  };
2196
2231
  var SHORT_FLAGS = {
@@ -2325,6 +2360,7 @@ function resolveContext({ flags, env, configDir }) {
2325
2360
  const key = pick(flagString(flags, "api-key"), env.SENTISENSE_API_KEY, stored.apiKey);
2326
2361
  const base = pick(flagString(flags, "base-url"), env.SENTISENSE_BASE_URL, stored.baseUrl);
2327
2362
  const agent = pick(flagString(flags, "agent"), env.SENTISENSE_AGENT_NAME, stored.agentName);
2363
+ const skill = pick(flagString(flags, "skill"), env.SENTISENSE_SKILL, stored.skill);
2328
2364
  return {
2329
2365
  configDir: dir,
2330
2366
  apiKey: key.value,
@@ -2332,21 +2368,27 @@ function resolveContext({ flags, env, configDir }) {
2332
2368
  baseUrl: base.value,
2333
2369
  baseUrlSource: base.source,
2334
2370
  agentName: agent.value,
2335
- agentSource: agent.source
2371
+ agentSource: agent.source,
2372
+ skill: skill.value,
2373
+ skillSource: skill.source
2336
2374
  };
2337
2375
  }
2338
2376
  var DEFAULT_BASE_URL2 = "https://app.sentisense.ai";
2339
2377
  function effectiveBaseUrl(context) {
2340
2378
  return context.baseUrl ?? DEFAULT_BASE_URL2;
2341
2379
  }
2342
- function sanitizeAgent(name) {
2343
- return name.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "");
2380
+ var MAX_IDENTITY = 32;
2381
+ function sanitizeIdentity(value) {
2382
+ return value.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "").replace(/-{2,}/g, "-").slice(0, MAX_IDENTITY).replace(/^-+|-+$/g, "");
2344
2383
  }
2345
2384
  function userAgentSuffix(context) {
2346
- const parts = [`sentisense-cli/${VERSION}`];
2347
- const agent = context.agentName ? sanitizeAgent(context.agentName) : "";
2348
- if (agent) parts.push(`agent/${agent}`);
2349
- return parts.join(" ");
2385
+ const product = `sentisense-cli/${VERSION}`;
2386
+ const slug = sanitizeIdentity(context.skill ?? "");
2387
+ const agent = sanitizeIdentity(context.agentName ?? "");
2388
+ const comment = [];
2389
+ if (slug) comment.push(slug);
2390
+ if (agent) comment.push(`agent/${agent}`);
2391
+ return comment.length === 0 ? product : `${product} (${comment.join("; ")})`;
2350
2392
  }
2351
2393
  function createClient(context, options = {}) {
2352
2394
  if (!options.anonymous && !context.apiKey) throw new MissingKeyError();
@@ -2752,13 +2794,20 @@ var newsCommand = {
2752
2794
  "A story is a cluster of articles covering the same event, not a single headline, so",
2753
2795
  "the size column is how many sources picked it up and impact ranks how much it moved.",
2754
2796
  "Tone is the average sentiment across the cluster, between -1 and 1.",
2755
- "--days only applies to the market-wide feed.",
2797
+ "--days only applies to the market-wide feed, and counts from when a story STARTED",
2798
+ "breaking, not from its latest article. A running story with fresh coverage but an",
2799
+ "older start falls out of short windows, so an empty window can be correct. It also",
2800
+ "switches ordering to curation score instead of the day-bucketed default.",
2756
2801
  EMPTY_VERIFY_NOTE,
2757
2802
  EMPTY_VERIFY_NOTE_2
2758
2803
  ],
2759
2804
  flags: {
2760
2805
  limit: { type: "number", placeholder: "N", describe: `Stories to return (default ${DEFAULT_LIMIT})` },
2761
- days: { type: "number", placeholder: "N", describe: "Look-back window, market-wide feed only" }
2806
+ days: {
2807
+ type: "number",
2808
+ placeholder: "N",
2809
+ describe: "Look-back window in days, market-wide feed only"
2810
+ }
2762
2811
  },
2763
2812
  async run({ args, client, full }) {
2764
2813
  const api = client();
@@ -2766,7 +2815,10 @@ var newsCommand = {
2766
2815
  const notes = [];
2767
2816
  const limit = typeof args.flags.limit === "number" ? args.flags.limit : DEFAULT_LIMIT;
2768
2817
  const days = typeof args.flags.days === "number" ? args.flags.days : void 0;
2769
- const stories = ticker ? await api.documents.getStoriesByTicker(ticker, { limit }) : await api.documents.getStories({ limit, ...days === void 0 ? {} : { days } });
2818
+ const stories = ticker ? await api.documents.getStoriesByTicker(ticker, { limit }) : await api.documents.getStories({
2819
+ limit,
2820
+ ...days === void 0 ? {} : { filterHours: days * 24 }
2821
+ });
2770
2822
  if (ticker && stories.length === 0) {
2771
2823
  const note = await verifyTickerOnEmpty(api, ticker);
2772
2824
  if (note) notes.push(note);
@@ -3000,7 +3052,8 @@ function single(row, full) {
3000
3052
  field("Mkt cap", humanize(quote.marketCap)),
3001
3053
  field("P/E", fixed(quote.peRatio)),
3002
3054
  field("EPS TTM", fixed(quote.epsTTM)),
3003
- field("Div yield", percent(quote.dividendYield, 2)),
3055
+ // A ratio, unlike `changePercent` and the other percentages in the same payload.
3056
+ field("Div yield", ratioPercent(quote.dividendYield)),
3004
3057
  field(
3005
3058
  "52w",
3006
3059
  `${fixed(quote.week52Low)} to ${fixed(quote.week52High)}`
@@ -3604,6 +3657,12 @@ function mainHelp() {
3604
3657
  "",
3605
3658
  'Run "sentisense help <command>" for flags, examples, and exit codes.',
3606
3659
  "",
3660
+ "Saying who is calling (optional):",
3661
+ " SENTISENSE_AGENT_NAME=<name> what your agent calls itself",
3662
+ " SENTISENSE_SKILL=<slug> the skill driving it",
3663
+ " Set either and requests carry that identity, so usage can be understood and the",
3664
+ " tools improved. Nothing needs them, and nothing is inferred when they are absent.",
3665
+ "",
3607
3666
  "Research data, not investment advice."
3608
3667
  ];
3609
3668
  return `${lines.join("\n")}
@@ -3872,7 +3931,7 @@ async function runCli(argv, io) {
3872
3931
  return result.exitCode ?? EXIT.OK;
3873
3932
  } catch (error) {
3874
3933
  const debug = argv.includes("--debug");
3875
- const report = describeError(error, debug);
3934
+ const report = describeError(error, debug, name);
3876
3935
  for (const line of report.lines) io.stderr(`${line}
3877
3936
  `);
3878
3937
  return report.exitCode;
package/dist/index.cjs CHANGED
@@ -1036,7 +1036,7 @@ var Trackers = class {
1036
1036
  };
1037
1037
 
1038
1038
  // src/version.ts
1039
- var VERSION = "0.43.0";
1039
+ var VERSION = "0.45.0";
1040
1040
 
1041
1041
  // src/client.ts
1042
1042
  var DEFAULT_BASE_URL = "https://app.sentisense.ai";