os-context 0.1.0 → 0.1.1

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 (3) hide show
  1. package/README.md +21 -6
  2. package/dist/index.js +423 -27
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -15,10 +15,24 @@ OS Context gives AI agents a quick, local snapshot of your machine (app, window,
15
15
 
16
16
  ## Install
17
17
 
18
+ **Run without installing (npx):**
18
19
  ```bash
19
- npm install
20
- npm run build
21
- # Or link globally: npm link
20
+ npx os-context --pretty
21
+ ```
22
+ Use the package name `os-context` with npx; the command is then `context` inside the package.
23
+
24
+ **Install globally (get `context` in your PATH):**
25
+ ```bash
26
+ npm i -g os-context
27
+ context --pretty
28
+ ```
29
+
30
+ **From source:**
31
+ ```bash
32
+ git clone https://github.com/treadiehq/os-context.git && cd os-context
33
+ npm install && npm run build
34
+ ./node_modules/.bin/context --pretty
35
+ # or: npm link then context --pretty
22
36
  ```
23
37
 
24
38
  ## Usage
@@ -26,19 +40,20 @@ npm run build
26
40
  **Default (safe core context only):**
27
41
 
28
42
  ```bash
29
- context
43
+ npx os-context
44
+ # or, after global install: context
30
45
  ```
31
46
 
32
47
  **Pretty-print with frontmost window and clipboard, redact sensitive fields:**
33
48
 
34
49
  ```bash
35
- context --pretty --frontmost-window --clipboard --redact
50
+ npx os-context --pretty --frontmost-window --clipboard --redact
36
51
  ```
37
52
 
38
53
  **All optional features and debug timings:**
39
54
 
40
55
  ```bash
41
- context --pretty --clipboard --frontmost-window --apps \
56
+ npx os-context --pretty --clipboard --frontmost-window --apps \
42
57
  --battery --network --calendar --reminders \
43
58
  --redact --debug
44
59
  ```
package/dist/index.js CHANGED
@@ -45,11 +45,17 @@ var NetworkSchema = z.object({
45
45
  var CalendarEventSchema = z.object({
46
46
  start: z.string(),
47
47
  end: z.string(),
48
- title: z.string(),
49
- location: z.string().optional()
48
+ title: z.string().optional(),
49
+ title_sha256: z.string().optional(),
50
+ title_length: z.number().optional(),
51
+ location: z.string().optional(),
52
+ location_sha256: z.string().optional(),
53
+ location_length: z.number().optional()
50
54
  });
51
55
  var ReminderSchema = z.object({
52
- title: z.string(),
56
+ title: z.string().optional(),
57
+ title_sha256: z.string().optional(),
58
+ title_length: z.number().optional(),
53
59
  due: z.string().optional(),
54
60
  list: z.string().optional()
55
61
  });
@@ -422,8 +428,55 @@ async function collectFrontmost(options) {
422
428
 
423
429
  // src/modules/apps.ts
424
430
  var MAX_APPS = 50;
425
- async function collectAppsDarwin(_timeoutMs) {
426
- return { data: [], timingMs: 0 };
431
+ var APPS_SCRIPT = `
432
+ tell application "System Events"
433
+ set out to ""
434
+ set procs to (every process whose background only is false)
435
+ set n to (count of procs)
436
+ if n > 50 then set n to 50
437
+ repeat with i from 1 to n
438
+ set p to item i of procs
439
+ set pname to name of p
440
+ set pid to unix id of p
441
+ set bid to ""
442
+ try
443
+ set bid to bundle identifier of p
444
+ end try
445
+ if bid is missing value then set bid to ""
446
+ set out to out & pname & tab & (pid as string) & tab & bid & return
447
+ end repeat
448
+ return out
449
+ end tell
450
+ `;
451
+ async function collectAppsDarwin(timeoutMs) {
452
+ const t = timer();
453
+ try {
454
+ const result = await run("osascript", ["-e", APPS_SCRIPT], { timeoutMs });
455
+ if (result.timedOut || !result.ok) {
456
+ return {
457
+ data: [],
458
+ error: result.timedOut ? { module: "apps", message: "Timeout", code: "timeout" } : { module: "apps", message: result.stderr.trim() || "Failed to list apps", code: "error" },
459
+ timingMs: t.elapsed()
460
+ };
461
+ }
462
+ const normalized = result.stdout.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
463
+ const lines = normalized.trim().split("\n").filter(Boolean);
464
+ const apps = lines.map((line) => {
465
+ const parts = line.split(" ");
466
+ const name = (parts[0] ?? "").replace(/\r/g, "").trim() || "unknown";
467
+ const pid = parseInt((parts[1] ?? "0").replace(/\r/g, ""), 10);
468
+ const bundle_id = (parts[2] ?? "").replace(/\r/g, "").trim() || name;
469
+ return { name, bundle_id, pid: Number.isNaN(pid) ? 0 : pid };
470
+ }).filter((a) => a.pid > 0);
471
+ return { data: apps, timingMs: t.elapsed() };
472
+ } catch (err) {
473
+ const message = err instanceof Error ? err.message : String(err);
474
+ return {
475
+ data: [],
476
+ error: { module: "apps", message, code: "error" },
477
+ timingMs: t.elapsed()
478
+ };
479
+ }
427
480
  }
428
481
  async function collectAppsLinux(timeoutMs) {
429
482
  const t = timer();
@@ -461,11 +514,35 @@ async function collectApps(timeoutMs) {
461
514
  }
462
515
 
463
516
  // src/modules/clipboard.ts
464
- async function collectClipboardDarwin(_options) {
465
- return {
466
- data: { available: false, types: [] },
467
- timingMs: 0
468
- };
517
+ async function collectClipboardDarwin(options) {
518
+ const t = timer();
519
+ const timeoutMs = options.timeoutMs;
520
+ const redact = options.redact;
521
+ try {
522
+ const result = await run("pbpaste", [], { timeoutMs });
523
+ const text = result.ok ? result.stdout ?? "" : "";
524
+ const types = text.length > 0 ? ["public.utf8-plain-text"] : [];
525
+ const data = {
526
+ available: result.ok,
527
+ types
528
+ };
529
+ if (text.length > 0) {
530
+ if (redact) {
531
+ const r = redactString(text);
532
+ data.text_sha256 = r.sha256;
533
+ data.text_length = r.length;
534
+ } else {
535
+ data.text = text;
536
+ data.text_length = text.length;
537
+ }
538
+ }
539
+ return { data, timingMs: t.elapsed() };
540
+ } catch {
541
+ return {
542
+ data: { available: false, types: [] },
543
+ timingMs: t.elapsed()
544
+ };
545
+ }
469
546
  }
470
547
  async function collectClipboardLinux(options) {
471
548
  const t = timer();
@@ -518,11 +595,38 @@ async function collectClipboard(options) {
518
595
 
519
596
  // src/modules/battery.ts
520
597
  import { readFile } from "fs/promises";
521
- async function collectBatteryDarwin(_timeoutMs) {
522
- return {
523
- data: { percentage: 0, is_charging: false, power_source: "unknown" },
524
- timingMs: 0
525
- };
598
+ function parsePmsetBatt(stdout) {
599
+ const line = stdout.split("\n").find((l) => l.includes("%") || l.includes("InternalBattery"));
600
+ const pctMatch = stdout.match(/(\d+)%/);
601
+ const percentage = pctMatch ? Math.min(1, Math.max(0, parseInt(pctMatch[1], 10) / 100)) : 0;
602
+ const lower = stdout.toLowerCase();
603
+ const is_charging = lower.includes("charging") || lower.includes("charged");
604
+ let power_source = "unknown";
605
+ if (lower.includes("ac power") || lower.includes("charging") || lower.includes("charged")) {
606
+ power_source = "ac";
607
+ } else if (lower.includes("battery power") || lower.includes("discharging")) {
608
+ power_source = "battery";
609
+ }
610
+ return { percentage, is_charging, power_source };
611
+ }
612
+ async function collectBatteryDarwin(timeoutMs) {
613
+ const t = timer();
614
+ try {
615
+ const result = await run("pmset", ["-g", "batt"], { timeoutMs });
616
+ if (result.timedOut || !result.ok) {
617
+ return {
618
+ data: { percentage: 0, is_charging: false, power_source: "unknown" },
619
+ timingMs: t.elapsed()
620
+ };
621
+ }
622
+ const data = parsePmsetBatt(result.stdout);
623
+ return { data, timingMs: t.elapsed() };
624
+ } catch {
625
+ return {
626
+ data: { percentage: 0, is_charging: false, power_source: "unknown" },
627
+ timingMs: t.elapsed()
628
+ };
629
+ }
526
630
  }
527
631
  async function collectBatteryLinux(_timeoutMs) {
528
632
  const t = timer();
@@ -556,12 +660,33 @@ async function collectBattery(timeoutMs) {
556
660
  }
557
661
 
558
662
  // src/modules/network.ts
559
- import { readFile as readFile2 } from "fs/promises";
560
- async function collectNetworkDarwin(_timeoutMs) {
561
- return {
562
- data: { primary_interface: "", has_internet: false },
563
- timingMs: 0
564
- };
663
+ async function collectNetworkDarwin(timeoutMs) {
664
+ const t = timer();
665
+ try {
666
+ const routeResult = await run("route", ["get", "default"], { timeoutMs });
667
+ const ifMatch = routeResult.ok ? routeResult.stdout.match(/interface:\s*(\S+)/) : null;
668
+ const primary_interface = ifMatch ? ifMatch[1].trim() : "";
669
+ let ssid;
670
+ if (primary_interface) {
671
+ const ssidResult = await run("networksetup", ["-getairportnetwork", primary_interface], { timeoutMs });
672
+ const ssidMatch = ssidResult.ok ? ssidResult.stdout.match(/Current Wi-Fi Network:\s*(.+)/) : null;
673
+ if (ssidMatch && ssidMatch[1].trim()) ssid = ssidMatch[1].trim();
674
+ }
675
+ let has_internet = false;
676
+ if (primary_interface) {
677
+ const ifconfigResult = await run("ifconfig", [primary_interface], { timeoutMs });
678
+ has_internet = ifconfigResult.ok && /inet\s+\d+\.\d+\.\d+\.\d+/.test(ifconfigResult.stdout);
679
+ }
680
+ const data = { primary_interface, ssid, has_internet };
681
+ return { data, timingMs: t.elapsed() };
682
+ } catch (err) {
683
+ const message = err instanceof Error ? err.message : String(err);
684
+ return {
685
+ data: { primary_interface: "", has_internet: false },
686
+ error: { module: "network", message, code: "error" },
687
+ timingMs: t.elapsed()
688
+ };
689
+ }
565
690
  }
566
691
  async function collectNetworkLinux(timeoutMs) {
567
692
  const t = timer();
@@ -578,12 +703,12 @@ async function collectNetworkLinux(timeoutMs) {
578
703
  let has_internet = false;
579
704
  if (primary_interface) {
580
705
  try {
706
+ const { readFile: readFile2 } = await import("fs/promises");
581
707
  const operstate = await readFile2(
582
708
  `/sys/class/net/${primary_interface}/operstate`,
583
709
  "utf8"
584
710
  ).then((s) => s.trim()).catch(() => "");
585
- const hasCarrier = operstate === "up";
586
- if (hasCarrier) {
711
+ if (operstate === "up") {
587
712
  const addrResult = await run("ip", ["-4", "addr", "show", primary_interface], { timeoutMs });
588
713
  has_internet = addrResult.ok && /inet\s+\d+\.\d+\.\d+\.\d+/.test(addrResult.stdout);
589
714
  }
@@ -612,19 +737,290 @@ async function collectNetwork(timeoutMs) {
612
737
  }
613
738
 
614
739
  // src/modules/calendar.ts
615
- async function collectCalendar(_options) {
740
+ var EVENT_SEP = "";
741
+ var FIELD_SEP = "";
742
+ var CALENDAR_DENIED_PATTERNS = [
743
+ /calendar/i,
744
+ /permission/i,
745
+ /privacy/i,
746
+ /not authorised/i,
747
+ /access denied/i
748
+ ];
749
+ function isCalendarDenied(stderr) {
750
+ return CALENDAR_DENIED_PATTERNS.some((p) => p.test(stderr));
751
+ }
752
+ var CALENDAR_SCRIPT = `
753
+ tell application "Calendar"
754
+ set startDate to current date
755
+ set endDate to startDate + (24 * 60 * 60)
756
+ set eventList to {}
757
+ repeat with aCal in calendars
758
+ try
759
+ set theseEvents to (every event of aCal whose start date >= startDate and start date <= endDate)
760
+ repeat with e in theseEvents
761
+ copy e to end of eventList
762
+ end repeat
763
+ end try
764
+ end repeat
765
+ set output to ""
766
+ set d31 to character id 31
767
+ set d30 to character id 30
768
+ set total to (count of eventList)
769
+ set maxToOutput to 20
770
+ if total > maxToOutput then set total to maxToOutput
771
+ repeat with i from 1 to total
772
+ set e to item i of eventList
773
+ set startStr to (start date of e as string)
774
+ set endStr to (end date of e as string)
775
+ set sumStr to (summary of e as string)
776
+ set locStr to ""
777
+ try
778
+ set locStr to (location of e as string)
779
+ end try
780
+ set sumSafe to ""
781
+ repeat with j from 1 to (length of sumStr)
782
+ set c to character j of sumStr
783
+ if c is d30 or c is d31 or c is return then
784
+ set sumSafe to sumSafe & " "
785
+ else
786
+ set sumSafe to sumSafe & c
787
+ end if
788
+ end repeat
789
+ set locSafe to ""
790
+ repeat with j from 1 to (length of locStr)
791
+ set c to character j of locStr
792
+ if c is d30 or c is d31 or c is return then
793
+ set locSafe to locSafe & " "
794
+ else
795
+ set locSafe to locSafe & c
796
+ end if
797
+ end repeat
798
+ set output to output & startStr & d31 & endStr & d31 & sumSafe & d31 & locSafe & d30
799
+ end repeat
800
+ return output
801
+ end tell
802
+ `;
803
+ function parseCalendarOutput(raw) {
804
+ const events = [];
805
+ const blocks = raw.split(EVENT_SEP).filter((b) => b.trim().length > 0);
806
+ for (const block of blocks) {
807
+ const parts = block.split(FIELD_SEP);
808
+ if (parts.length >= 4) {
809
+ events.push({
810
+ start: parts[0].trim(),
811
+ end: parts[1].trim(),
812
+ title: parts[2].trim(),
813
+ location: parts[3].trim() || void 0
814
+ });
815
+ }
816
+ }
817
+ events.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
818
+ return events.slice(0, 3);
819
+ }
820
+ async function collectCalendar(options) {
616
821
  if (getPlatform() === "linux") {
617
822
  return { data: [], timingMs: 0 };
618
823
  }
619
- return { data: [], timingMs: 0 };
824
+ const t = timer();
825
+ const timeoutMs = options.timeoutMs;
826
+ const redact = options.redact;
827
+ const warnings = [];
828
+ try {
829
+ const result = await run("osascript", ["-e", CALENDAR_SCRIPT], { timeoutMs });
830
+ if (result.timedOut) {
831
+ return {
832
+ data: [],
833
+ warnings: [...warnings, "calendar: timeout"],
834
+ error: { module: "calendar", message: "Timeout", code: "timeout" },
835
+ timingMs: t.elapsed()
836
+ };
837
+ }
838
+ if (!result.ok) {
839
+ if (isCalendarDenied(result.stderr)) {
840
+ return {
841
+ data: [],
842
+ permission: "denied",
843
+ error: {
844
+ module: "calendar",
845
+ message: "Calendar permission required",
846
+ code: "permission_denied"
847
+ },
848
+ timingMs: t.elapsed()
849
+ };
850
+ }
851
+ return {
852
+ data: [],
853
+ error: {
854
+ module: "calendar",
855
+ message: result.stderr.trim() || "Failed to get calendar events",
856
+ code: "error"
857
+ },
858
+ timingMs: t.elapsed()
859
+ };
860
+ }
861
+ const raw = result.stdout?.trim() ?? "";
862
+ let events = raw ? parseCalendarOutput(raw) : [];
863
+ if (redact && events.length > 0) {
864
+ events = events.map((ev) => {
865
+ const titleRedacted = redactString(ev.title);
866
+ const out = {
867
+ start: ev.start,
868
+ end: ev.end,
869
+ title_sha256: titleRedacted.sha256,
870
+ title_length: titleRedacted.length
871
+ };
872
+ if (ev.location != null && ev.location !== "") {
873
+ const locRedacted = redactString(ev.location);
874
+ out.location_sha256 = locRedacted.sha256;
875
+ out.location_length = locRedacted.length;
876
+ }
877
+ return out;
878
+ });
879
+ }
880
+ return {
881
+ data: events,
882
+ permission: "granted",
883
+ timingMs: t.elapsed()
884
+ };
885
+ } catch (err) {
886
+ const message = err instanceof Error ? err.message : String(err);
887
+ return {
888
+ data: [],
889
+ error: { module: "calendar", message, code: "error" },
890
+ timingMs: t.elapsed()
891
+ };
892
+ }
620
893
  }
621
894
 
622
895
  // src/modules/reminders.ts
623
- async function collectReminders(_options) {
896
+ var REMINDER_SEP = "";
897
+ var FIELD_SEP2 = "";
898
+ var REMINDERS_DENIED_PATTERNS = [
899
+ /reminders/i,
900
+ /permission/i,
901
+ /privacy/i,
902
+ /not authorised/i,
903
+ /access denied/i
904
+ ];
905
+ function isRemindersDenied(stderr) {
906
+ return REMINDERS_DENIED_PATTERNS.some((p) => p.test(stderr));
907
+ }
908
+ var REMINDERS_SCRIPT = `
909
+ tell application "Reminders"
910
+ set startDate to current date
911
+ set endDate to startDate + (7 * 24 * 60 * 60)
912
+ set out to ""
913
+ set d31 to character id 31
914
+ set d30 to character id 30
915
+ set count to 0
916
+ repeat with aList in lists
917
+ try
918
+ set theReminders to (every reminder of aList whose completed is false and (due date is not missing value) and (due date >= startDate) and (due date <= endDate))
919
+ repeat with r in theReminders
920
+ if count >= 10 then exit repeat
921
+ set titleStr to (name of r as string)
922
+ set dueStr to ""
923
+ try
924
+ set dueStr to (due date of r as string)
925
+ end try
926
+ set listStr to (name of aList as string)
927
+ set titleSafe to ""
928
+ repeat with j from 1 to (length of titleStr)
929
+ set c to character j of titleStr
930
+ if c is d30 or c is d31 or c is return then
931
+ set titleSafe to titleSafe & " "
932
+ else
933
+ set titleSafe to titleSafe & c
934
+ end if
935
+ end repeat
936
+ set out to out & titleSafe & d31 & dueStr & d31 & listStr & d30
937
+ set count to count + 1
938
+ end repeat
939
+ end try
940
+ end repeat
941
+ return out
942
+ end tell
943
+ `;
944
+ function parseRemindersOutput(raw) {
945
+ const reminders = [];
946
+ const blocks = raw.split(REMINDER_SEP).filter((b) => b.trim().length > 0);
947
+ for (const block of blocks) {
948
+ const parts = block.split(FIELD_SEP2);
949
+ if (parts.length >= 3) {
950
+ reminders.push({
951
+ title: parts[0].trim(),
952
+ due: parts[1].trim() || void 0,
953
+ list: parts[2].trim() || void 0
954
+ });
955
+ }
956
+ }
957
+ return reminders;
958
+ }
959
+ async function collectReminders(options) {
624
960
  if (getPlatform() === "linux") {
625
961
  return { data: [], timingMs: 0 };
626
962
  }
627
- return { data: [], timingMs: 0 };
963
+ const t = timer();
964
+ const timeoutMs = options.timeoutMs;
965
+ const redact = options.redact;
966
+ try {
967
+ const result = await run("osascript", ["-e", REMINDERS_SCRIPT], { timeoutMs });
968
+ if (result.timedOut) {
969
+ return {
970
+ data: [],
971
+ error: { module: "reminders", message: "Timeout", code: "timeout" },
972
+ timingMs: t.elapsed()
973
+ };
974
+ }
975
+ if (!result.ok) {
976
+ if (isRemindersDenied(result.stderr)) {
977
+ return {
978
+ data: [],
979
+ permission: "denied",
980
+ error: {
981
+ module: "reminders",
982
+ message: "Reminders permission required",
983
+ code: "permission_denied"
984
+ },
985
+ timingMs: t.elapsed()
986
+ };
987
+ }
988
+ return {
989
+ data: [],
990
+ error: {
991
+ module: "reminders",
992
+ message: result.stderr.trim() || "Failed to get reminders",
993
+ code: "error"
994
+ },
995
+ timingMs: t.elapsed()
996
+ };
997
+ }
998
+ const raw = result.stdout?.trim() ?? "";
999
+ let reminders = raw ? parseRemindersOutput(raw) : [];
1000
+ if (redact && reminders.length > 0) {
1001
+ reminders = reminders.map((r) => {
1002
+ const titleRedacted = redactString(r.title ?? "");
1003
+ return {
1004
+ title_sha256: titleRedacted.sha256,
1005
+ title_length: titleRedacted.length,
1006
+ due: r.due,
1007
+ list: r.list
1008
+ };
1009
+ });
1010
+ }
1011
+ return {
1012
+ data: reminders,
1013
+ permission: "granted",
1014
+ timingMs: t.elapsed()
1015
+ };
1016
+ } catch (err) {
1017
+ const message = err instanceof Error ? err.message : String(err);
1018
+ return {
1019
+ data: [],
1020
+ error: { module: "reminders", message, code: "error" },
1021
+ timingMs: t.elapsed()
1022
+ };
1023
+ }
628
1024
  }
629
1025
 
630
1026
  // src/util/json.ts
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "os-context",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Fast macOS & Linux CLI that prints a single JSON object describing your current local context for agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
- "context": "dist/index.js"
8
+ "context": "dist/index.js",
9
+ "os-context": "dist/index.js"
9
10
  },
10
11
  "files": ["dist"],
11
12
  "scripts": {