@bretwardjames/tw-bridge 0.4.0 → 0.6.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/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs3 from "fs";
5
- import path4 from "path";
6
- import os3 from "os";
4
+ import fs5 from "fs";
5
+ import path6 from "path";
6
+ import os5 from "os";
7
7
 
8
8
  // src/config.ts
9
9
  import fs from "fs";
@@ -241,8 +241,8 @@ async function oauthLogin(clientId, clientSecret, rl) {
241
241
  saveTokens(tokens);
242
242
  return tokens;
243
243
  }
244
- async function asanaFetch(path5, token, options = {}) {
245
- const url = `${API_BASE}${path5}`;
244
+ async function asanaFetch(path7, token, options = {}) {
245
+ const url = `${API_BASE}${path7}`;
246
246
  const res = await fetch(url, {
247
247
  ...options,
248
248
  headers: {
@@ -258,9 +258,9 @@ async function asanaFetch(path5, token, options = {}) {
258
258
  const json = await res.json();
259
259
  return json.data;
260
260
  }
261
- async function asanaFetchAll(path5, token) {
261
+ async function asanaFetchAll(path7, token) {
262
262
  const results = [];
263
- let nextPage = path5;
263
+ let nextPage = path7;
264
264
  while (nextPage) {
265
265
  const url = `${API_BASE}${nextPage}`;
266
266
  const res = await fetch(url, {
@@ -572,10 +572,319 @@ function mapAsanaPriority(fields) {
572
572
  return void 0;
573
573
  }
574
574
 
575
+ // src/adapters/strety.ts
576
+ import fs3 from "fs";
577
+ import path4 from "path";
578
+ import os3 from "os";
579
+ import { execSync as execSync2 } from "child_process";
580
+ import { createInterface as createInterface2 } from "readline/promises";
581
+ import { stdin as stdin2, stdout as stdout2 } from "process";
582
+ var API_BASE2 = "https://2.strety.com/api/v1";
583
+ var OAUTH_AUTHORIZE2 = `${API_BASE2}/oauth/authorize`;
584
+ var OAUTH_TOKEN2 = `${API_BASE2}/oauth/token`;
585
+ var TOKEN_FILE2 = path4.join(os3.homedir(), ".config", "tw-bridge", "strety-tokens.json");
586
+ function loadTokens2() {
587
+ try {
588
+ return JSON.parse(fs3.readFileSync(TOKEN_FILE2, "utf-8"));
589
+ } catch {
590
+ return null;
591
+ }
592
+ }
593
+ function saveTokens2(tokens) {
594
+ fs3.mkdirSync(path4.dirname(TOKEN_FILE2), { recursive: true });
595
+ fs3.writeFileSync(TOKEN_FILE2, JSON.stringify(tokens, null, 2), { mode: 384 });
596
+ }
597
+ async function refreshAccessToken2(tokens) {
598
+ const now = Date.now();
599
+ if (tokens.access_token && tokens.expires_at > now + 3e5) {
600
+ return tokens.access_token;
601
+ }
602
+ const res = await fetch(OAUTH_TOKEN2, {
603
+ method: "POST",
604
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
605
+ body: new URLSearchParams({
606
+ grant_type: "refresh_token",
607
+ refresh_token: tokens.refresh_token,
608
+ client_id: tokens.client_id,
609
+ client_secret: tokens.client_secret
610
+ })
611
+ });
612
+ if (!res.ok) {
613
+ const body = await res.text();
614
+ throw new Error(`Token refresh failed (${res.status}): ${body}`);
615
+ }
616
+ const data = await res.json();
617
+ tokens.access_token = data.access_token;
618
+ if (data.refresh_token) {
619
+ tokens.refresh_token = data.refresh_token;
620
+ }
621
+ tokens.expires_at = now + (data.expires_in ?? 7200) * 1e3;
622
+ saveTokens2(tokens);
623
+ return tokens.access_token;
624
+ }
625
+ var REDIRECT_URI = "https://localhost/callback";
626
+ async function oauthLogin2(clientId, clientSecret, rl) {
627
+ const authUrl = `${OAUTH_AUTHORIZE2}?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=${encodeURIComponent("read write")}`;
628
+ console.log(`
629
+ Opening browser for authorization...`);
630
+ console.log(` If it doesn't open, visit:
631
+ ${authUrl}
632
+ `);
633
+ try {
634
+ execSync2(`xdg-open "${authUrl}" 2>/dev/null || open "${authUrl}" 2>/dev/null`, {
635
+ stdio: "ignore"
636
+ });
637
+ } catch {
638
+ }
639
+ console.log(" After authorizing, the browser will redirect to a page that");
640
+ console.log(" won't load. Copy the full URL from the address bar and paste it here.\n");
641
+ const pasted = (await rl.question("Paste the callback URL: ")).trim();
642
+ let code;
643
+ try {
644
+ const url = new URL(pasted);
645
+ const error = url.searchParams.get("error");
646
+ if (error) {
647
+ throw new Error(`OAuth error: ${error}`);
648
+ }
649
+ const authCode = url.searchParams.get("code");
650
+ if (!authCode) {
651
+ throw new Error("No authorization code found in URL");
652
+ }
653
+ code = authCode;
654
+ } catch (err) {
655
+ if (err instanceof Error && err.message.startsWith("OAuth error:")) throw err;
656
+ if (err instanceof Error && err.message.startsWith("No authorization")) throw err;
657
+ throw new Error(`Could not parse callback URL: ${pasted}`);
658
+ }
659
+ const tokenRes = await fetch(OAUTH_TOKEN2, {
660
+ method: "POST",
661
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
662
+ body: new URLSearchParams({
663
+ grant_type: "authorization_code",
664
+ code,
665
+ redirect_uri: REDIRECT_URI,
666
+ client_id: clientId,
667
+ client_secret: clientSecret
668
+ })
669
+ });
670
+ if (!tokenRes.ok) {
671
+ const body = await tokenRes.text();
672
+ throw new Error(`Token exchange failed (${tokenRes.status}): ${body}`);
673
+ }
674
+ const data = await tokenRes.json();
675
+ const tokens = {
676
+ access_token: data.access_token,
677
+ refresh_token: data.refresh_token,
678
+ expires_at: Date.now() + (data.expires_in ?? 7200) * 1e3,
679
+ client_id: clientId,
680
+ client_secret: clientSecret
681
+ };
682
+ saveTokens2(tokens);
683
+ return tokens;
684
+ }
685
+ async function stretyFetch(urlOrPath, token, options = {}) {
686
+ const url = urlOrPath.startsWith("http") ? urlOrPath : `${API_BASE2}${urlOrPath}`;
687
+ const res = await fetch(url, {
688
+ ...options,
689
+ headers: {
690
+ Authorization: `Bearer ${token}`,
691
+ Accept: "application/vnd.api+json",
692
+ "Content-Type": "application/vnd.api+json",
693
+ ...options.headers
694
+ }
695
+ });
696
+ if (!res.ok) {
697
+ const body = await res.text();
698
+ throw new Error(`Strety API ${res.status}: ${body}`);
699
+ }
700
+ return res.json();
701
+ }
702
+ async function stretyFetchAll(path7, token) {
703
+ const results = [];
704
+ let url = `${API_BASE2}${path7}`;
705
+ while (url) {
706
+ const json = await stretyFetch(url, token);
707
+ results.push(...json.data);
708
+ url = json.links?.next ?? null;
709
+ }
710
+ return results;
711
+ }
712
+ async function pickOne2(rl, label, items) {
713
+ console.log(`
714
+ ${label}:`);
715
+ for (let i = 0; i < items.length; i++) {
716
+ const name = items[i].attributes.name ?? items[i].attributes.title ?? items[i].id;
717
+ console.log(` ${i + 1}) ${name}`);
718
+ }
719
+ while (true) {
720
+ const answer = await rl.question(`Choose (1-${items.length}): `);
721
+ const idx = parseInt(answer, 10) - 1;
722
+ if (idx >= 0 && idx < items.length) {
723
+ const item = items[idx];
724
+ return { id: item.id, name: item.attributes.name ?? item.attributes.title ?? item.id };
725
+ }
726
+ console.log(" Invalid choice, try again.");
727
+ }
728
+ }
729
+ var StretyAdapter = class {
730
+ name = "strety";
731
+ config;
732
+ accessToken = null;
733
+ async resolveToken() {
734
+ const auth = this.config.auth;
735
+ if (auth === "oauth") {
736
+ const tokens = loadTokens2();
737
+ if (!tokens) throw new Error("No OAuth tokens found. Run `tw-bridge add` to authenticate.");
738
+ this.accessToken = await refreshAccessToken2(tokens);
739
+ return this.accessToken;
740
+ }
741
+ if (this.accessToken) return this.accessToken;
742
+ if (auth.startsWith("env:")) {
743
+ const envVar = auth.slice(4);
744
+ const val = process.env[envVar];
745
+ if (!val) throw new Error(`Environment variable ${envVar} not set`);
746
+ this.accessToken = val;
747
+ return val;
748
+ }
749
+ this.accessToken = auth;
750
+ return auth;
751
+ }
752
+ defaultConfig(_cwd) {
753
+ return {
754
+ auth: "env:STRETY_TOKEN",
755
+ assignee_id: "",
756
+ assignee_name: ""
757
+ };
758
+ }
759
+ async setup(_cwd) {
760
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
761
+ try {
762
+ console.log("\n\u2500\u2500 Strety Adapter Setup \u2500\u2500\n");
763
+ console.log("Authentication:");
764
+ console.log(" 1) OAuth (browser login)");
765
+ console.log(" 2) Environment variable");
766
+ const authChoice = (await rl.question("\nChoose (1-2): ")).trim();
767
+ let authConfig;
768
+ let token;
769
+ if (authChoice === "1") {
770
+ console.log(`
771
+ Register a Strety app at My Integrations > My Apps.`);
772
+ console.log(` Set the Redirect URI to: ${REDIRECT_URI}
773
+ `);
774
+ const clientId = (await rl.question("Strety App Client ID: ")).trim();
775
+ const clientSecret = (await rl.question("Strety App Client Secret: ")).trim();
776
+ if (!clientId || !clientSecret) {
777
+ console.error("\n Client ID and Secret are required.");
778
+ rl.close();
779
+ process.exit(1);
780
+ }
781
+ const tokens = await oauthLogin2(clientId, clientSecret, rl);
782
+ token = tokens.access_token;
783
+ authConfig = "oauth";
784
+ } else {
785
+ const envVar = await rl.question("Environment variable name [STRETY_TOKEN]: ");
786
+ const varName = envVar.trim() || "STRETY_TOKEN";
787
+ authConfig = `env:${varName}`;
788
+ token = process.env[varName] ?? "";
789
+ if (!token) {
790
+ console.error(`
791
+ Cannot complete setup without a valid token.`);
792
+ console.error(` Set ${varName} in your environment and try again.`);
793
+ rl.close();
794
+ process.exit(1);
795
+ }
796
+ }
797
+ console.log("\nVerifying...");
798
+ let people;
799
+ try {
800
+ people = await stretyFetchAll("/people?page[size]=20", token);
801
+ } catch (err) {
802
+ const msg = err instanceof Error ? err.message : String(err);
803
+ console.error(` Authentication failed: ${msg}`);
804
+ rl.close();
805
+ process.exit(1);
806
+ }
807
+ console.log(` Connected! Found ${people.length} people.`);
808
+ const person = await pickOne2(rl, "Which person are you? (for filtering your todos)", people);
809
+ rl.close();
810
+ console.log(`
811
+ Assignee: ${person.name}`);
812
+ console.log(" Syncing: Todos assigned to you");
813
+ return {
814
+ auth: authConfig,
815
+ assignee_id: person.id,
816
+ assignee_name: person.name
817
+ };
818
+ } catch (err) {
819
+ rl.close();
820
+ throw err;
821
+ }
822
+ }
823
+ async init(config) {
824
+ this.config = config;
825
+ if (!this.config.assignee_id) {
826
+ throw new Error('strety adapter requires "assignee_id" in config');
827
+ }
828
+ }
829
+ async pull() {
830
+ const token = await this.resolveToken();
831
+ const raw = await stretyFetchAll(
832
+ `/todos?filter[assignee_id]=${this.config.assignee_id}&page[size]=20`,
833
+ token
834
+ );
835
+ const tasks = [];
836
+ for (const item of raw) {
837
+ const attrs = item.attributes;
838
+ if (attrs.completed_at) continue;
839
+ const priority = mapStretyPriority(attrs.priority);
840
+ const task = {
841
+ uuid: "",
842
+ description: attrs.title,
843
+ status: "pending",
844
+ entry: attrs.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
845
+ tags: [],
846
+ priority,
847
+ backend: "",
848
+ backend_id: item.id,
849
+ ...attrs.due_date && { due: new Date(attrs.due_date).toISOString() }
850
+ };
851
+ tasks.push(task);
852
+ }
853
+ return tasks;
854
+ }
855
+ async onDone(task) {
856
+ const todoId = task.backend_id;
857
+ if (!todoId) return;
858
+ const token = await this.resolveToken();
859
+ process.stderr.write(`tw-bridge [strety]: marking todo complete
860
+ `);
861
+ await stretyFetch(`/todos/${todoId}`, token, {
862
+ method: "PATCH",
863
+ headers: { "If-Match": "*" },
864
+ body: JSON.stringify({
865
+ data: {
866
+ type: "todo",
867
+ attributes: {
868
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
869
+ }
870
+ }
871
+ })
872
+ });
873
+ }
874
+ };
875
+ function mapStretyPriority(priority) {
876
+ if (!priority || priority === "none") return void 0;
877
+ if (priority === "highest" || priority === "high") return "H";
878
+ if (priority === "medium") return "M";
879
+ if (priority === "low" || priority === "lowest") return "L";
880
+ return void 0;
881
+ }
882
+
575
883
  // src/registry.ts
576
884
  var BUILTIN_ADAPTERS = {
577
885
  ghp: () => new GhpAdapter(),
578
- asana: () => new AsanaAdapter()
886
+ asana: () => new AsanaAdapter(),
887
+ strety: () => new StretyAdapter()
579
888
  };
580
889
  function matchBackend(task, config) {
581
890
  if (task.backend && config.backends[task.backend]) {
@@ -707,12 +1016,65 @@ function updateTaskDescription(existing, newDescription) {
707
1016
  return result.status === 0;
708
1017
  }
709
1018
 
1019
+ // src/tracking.ts
1020
+ import fs4 from "fs";
1021
+ import path5 from "path";
1022
+ import os4 from "os";
1023
+ import { spawnSync as spawnSync3 } from "child_process";
1024
+ var TRACKING_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", "tracking.json");
1025
+ function loadTracking() {
1026
+ try {
1027
+ return JSON.parse(fs4.readFileSync(TRACKING_FILE, "utf-8"));
1028
+ } catch {
1029
+ return {};
1030
+ }
1031
+ }
1032
+ function saveTracking(state) {
1033
+ fs4.mkdirSync(path5.dirname(TRACKING_FILE), { recursive: true });
1034
+ fs4.writeFileSync(TRACKING_FILE, JSON.stringify(state));
1035
+ }
1036
+ function mergedTags(state) {
1037
+ const all = /* @__PURE__ */ new Set();
1038
+ for (const tags of Object.values(state)) {
1039
+ for (const tag of tags) all.add(tag);
1040
+ }
1041
+ return [...all];
1042
+ }
1043
+ function startParallel(key, tags) {
1044
+ const tracking = loadTracking();
1045
+ tracking[key] = tags;
1046
+ saveTracking(tracking);
1047
+ const merged = mergedTags(tracking);
1048
+ spawnSync3("timew", ["stop"], { stdio: "pipe" });
1049
+ spawnSync3("timew", ["start", ...merged], { stdio: "pipe" });
1050
+ }
1051
+ function startSwitch(key, tags) {
1052
+ saveTracking({ [key]: tags });
1053
+ spawnSync3("timew", ["stop"], { stdio: "pipe" });
1054
+ spawnSync3("timew", ["start", ...tags], { stdio: "pipe" });
1055
+ }
1056
+ function stopEntry(key) {
1057
+ const tracking = loadTracking();
1058
+ delete tracking[key];
1059
+ saveTracking(tracking);
1060
+ const remaining = mergedTags(tracking);
1061
+ spawnSync3("timew", ["stop"], { stdio: "pipe" });
1062
+ if (remaining.length > 0) {
1063
+ spawnSync3("timew", ["start", ...remaining], { stdio: "pipe" });
1064
+ }
1065
+ }
1066
+ function getActiveMeetings() {
1067
+ const tracking = loadTracking();
1068
+ return Object.entries(tracking).filter(([key]) => key.startsWith("meeting:")).map(([key, tags]) => ({ key, tags }));
1069
+ }
1070
+
710
1071
  // src/cli.ts
711
- var HOOKS_DIR = path4.join(os3.homedir(), ".task", "hooks");
1072
+ var HOOKS_DIR = path6.join(os5.homedir(), ".task", "hooks");
712
1073
  var commands = {
713
1074
  add: addBackend,
714
1075
  install,
715
1076
  sync,
1077
+ meeting: meetingCmd,
716
1078
  timewarrior: timewarriorCmd,
717
1079
  which,
718
1080
  config: showConfig
@@ -722,9 +1084,10 @@ async function main() {
722
1084
  if (!command || command === "--help") {
723
1085
  console.log("Usage: tw-bridge <command>\n");
724
1086
  console.log("Commands:");
725
- console.log(" add Add a new backend instance");
726
- console.log(" install Install Taskwarrior hooks and shell integration");
1087
+ console.log(" add Add a new backend instance");
1088
+ console.log(" install Install Taskwarrior hooks and shell integration");
727
1089
  console.log(" sync Pull tasks from all backends");
1090
+ console.log(" meeting Track meetings in Timewarrior (no task created)");
728
1091
  console.log(" timewarrior Manage Timewarrior integration");
729
1092
  console.log(" which Print the context for the current directory");
730
1093
  console.log(" config Show current configuration");
@@ -800,18 +1163,18 @@ Added backend "${name}" (adapter: ${adapterType})`);
800
1163
  Use 'task context ${matchTag}' to switch to this project.`);
801
1164
  }
802
1165
  async function install() {
803
- fs3.mkdirSync(HOOKS_DIR, { recursive: true });
804
- const hookSource = path4.resolve(
805
- path4.dirname(new URL(import.meta.url).pathname),
1166
+ fs5.mkdirSync(HOOKS_DIR, { recursive: true });
1167
+ const hookSource = path6.resolve(
1168
+ path6.dirname(new URL(import.meta.url).pathname),
806
1169
  "hooks",
807
1170
  "on-modify.js"
808
1171
  );
809
- const hookTarget = path4.join(HOOKS_DIR, "on-modify.tw-bridge");
810
- if (fs3.existsSync(hookTarget)) {
811
- fs3.unlinkSync(hookTarget);
1172
+ const hookTarget = path6.join(HOOKS_DIR, "on-modify.tw-bridge");
1173
+ if (fs5.existsSync(hookTarget)) {
1174
+ fs5.unlinkSync(hookTarget);
812
1175
  }
813
- fs3.symlinkSync(hookSource, hookTarget);
814
- fs3.chmodSync(hookSource, 493);
1176
+ fs5.symlinkSync(hookSource, hookTarget);
1177
+ fs5.chmodSync(hookSource, 493);
815
1178
  console.log(`Installed hook: ${hookTarget} -> ${hookSource}`);
816
1179
  console.log("\nAdd these to your .taskrc:\n");
817
1180
  console.log("# --- tw-bridge UDAs ---");
@@ -828,31 +1191,133 @@ async function install() {
828
1191
  console.log("urgency.user.tag.ready_for_beta.coefficient=-4.0");
829
1192
  console.log("urgency.user.tag.in_beta.coefficient=-6.0");
830
1193
  installTimewExtension();
831
- if (fs3.existsSync(STANDARD_TIMEW_HOOK)) {
1194
+ if (fs5.existsSync(STANDARD_TIMEW_HOOK)) {
832
1195
  console.log("\nTimewarrior hook detected. To enable tw-bridge time tracking:");
833
1196
  console.log(" tw-bridge timewarrior enable");
834
1197
  }
835
1198
  installShellFunction();
836
1199
  }
837
- var TIMEW_EXT_DIR = path4.join(os3.homedir(), ".timewarrior", "extensions");
1200
+ var TIMEW_EXT_DIR = path6.join(os5.homedir(), ".timewarrior", "extensions");
838
1201
  function installTimewExtension() {
839
- fs3.mkdirSync(TIMEW_EXT_DIR, { recursive: true });
840
- const extSource = path4.resolve(
841
- path4.dirname(new URL(import.meta.url).pathname),
1202
+ fs5.mkdirSync(TIMEW_EXT_DIR, { recursive: true });
1203
+ const extSource = path6.resolve(
1204
+ path6.dirname(new URL(import.meta.url).pathname),
842
1205
  "extensions",
843
1206
  "bridge.js"
844
1207
  );
845
- const extTarget = path4.join(TIMEW_EXT_DIR, "bridge");
846
- if (fs3.existsSync(extTarget)) {
847
- fs3.unlinkSync(extTarget);
1208
+ const extTarget = path6.join(TIMEW_EXT_DIR, "bridge");
1209
+ if (fs5.existsSync(extTarget)) {
1210
+ fs5.unlinkSync(extTarget);
848
1211
  }
849
- fs3.symlinkSync(extSource, extTarget);
850
- fs3.chmodSync(extSource, 493);
1212
+ fs5.symlinkSync(extSource, extTarget);
1213
+ fs5.chmodSync(extSource, 493);
851
1214
  console.log(`
852
1215
  Installed Timewarrior extension: ${extTarget} -> ${extSource}`);
853
1216
  console.log(" Usage: timew bridge [task-time|wall-time] [project-filter]");
854
1217
  }
855
- var STANDARD_TIMEW_HOOK = path4.join(HOOKS_DIR, "on-modify.timewarrior");
1218
+ function sanitizeMeetingName(name) {
1219
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1220
+ }
1221
+ function detectProjectContext() {
1222
+ const cwd = process.cwd();
1223
+ const config = loadConfig();
1224
+ for (const [, backend] of Object.entries(config.backends)) {
1225
+ const backendCwd = backend.config?.cwd;
1226
+ if (!backendCwd) continue;
1227
+ const resolved = path6.resolve(backendCwd);
1228
+ if (cwd === resolved || cwd.startsWith(resolved + path6.sep)) {
1229
+ return backend.match.tags?.[0] ?? null;
1230
+ }
1231
+ }
1232
+ return null;
1233
+ }
1234
+ async function meetingCmd() {
1235
+ const sub = process.argv[3];
1236
+ if (!sub || sub === "--help") {
1237
+ console.log("Usage: tw-bridge meeting <subcommand>\n");
1238
+ console.log("Subcommands:");
1239
+ console.log(" start <name> [--switch] Start tracking a meeting");
1240
+ console.log(" stop [name] Stop a meeting (or all if no name)");
1241
+ console.log(" list Show active meetings");
1242
+ console.log("\nMeetings are tracked in Timewarrior only \u2014 no Taskwarrior task is created.");
1243
+ console.log("By default, meetings run in parallel with active tasks.");
1244
+ console.log("Use --switch to pause the current task instead.");
1245
+ return;
1246
+ }
1247
+ const config = loadConfig();
1248
+ if (!config.timewarrior?.enabled) {
1249
+ console.error("Timewarrior is not enabled. Run: tw-bridge timewarrior enable");
1250
+ process.exit(1);
1251
+ }
1252
+ if (sub === "start") {
1253
+ const nameArg = process.argv.slice(4).filter((a) => !a.startsWith("--")).join(" ");
1254
+ if (!nameArg) {
1255
+ console.error("Usage: tw-bridge meeting start <name>");
1256
+ process.exit(1);
1257
+ }
1258
+ const switchMode = process.argv.includes("--switch") || process.argv.includes("-s");
1259
+ const tag = sanitizeMeetingName(nameArg);
1260
+ const key = `meeting:${tag}`;
1261
+ const tags = ["meeting", tag];
1262
+ const project = detectProjectContext();
1263
+ if (project) tags.push(project);
1264
+ if (switchMode) {
1265
+ startSwitch(key, tags);
1266
+ } else {
1267
+ startParallel(key, tags);
1268
+ }
1269
+ console.log(`Meeting started: ${nameArg}`);
1270
+ console.log(` Tags: ${tags.join(" ")}`);
1271
+ if (!switchMode) {
1272
+ console.log(" Mode: parallel (active tasks continue tracking)");
1273
+ } else {
1274
+ console.log(" Mode: switch (active tasks paused)");
1275
+ }
1276
+ return;
1277
+ }
1278
+ if (sub === "stop") {
1279
+ const nameArg = process.argv.slice(4).join(" ").trim();
1280
+ const active = getActiveMeetings();
1281
+ if (active.length === 0) {
1282
+ console.log("No active meetings.");
1283
+ return;
1284
+ }
1285
+ if (nameArg) {
1286
+ const tag = sanitizeMeetingName(nameArg);
1287
+ const key = `meeting:${tag}`;
1288
+ const match = active.find((m) => m.key === key);
1289
+ if (!match) {
1290
+ console.error(`No active meeting matching "${nameArg}".`);
1291
+ console.error(`Active meetings: ${active.map((m) => m.key.replace("meeting:", "")).join(", ")}`);
1292
+ process.exit(1);
1293
+ }
1294
+ stopEntry(key);
1295
+ console.log(`Meeting stopped: ${nameArg}`);
1296
+ } else {
1297
+ for (const m of active) {
1298
+ stopEntry(m.key);
1299
+ }
1300
+ console.log(`Stopped ${active.length} meeting(s): ${active.map((m) => m.key.replace("meeting:", "")).join(", ")}`);
1301
+ }
1302
+ return;
1303
+ }
1304
+ if (sub === "list") {
1305
+ const active = getActiveMeetings();
1306
+ if (active.length === 0) {
1307
+ console.log("No active meetings.");
1308
+ return;
1309
+ }
1310
+ console.log("Active meetings:");
1311
+ for (const m of active) {
1312
+ const name = m.key.replace("meeting:", "");
1313
+ console.log(` ${name} (${m.tags.join(" ")})`);
1314
+ }
1315
+ return;
1316
+ }
1317
+ console.error(`Unknown subcommand: ${sub}`);
1318
+ process.exit(1);
1319
+ }
1320
+ var STANDARD_TIMEW_HOOK = path6.join(HOOKS_DIR, "on-modify.timewarrior");
856
1321
  async function timewarriorCmd() {
857
1322
  const sub = process.argv[3];
858
1323
  if (!sub || sub === "--help") {
@@ -875,8 +1340,8 @@ async function timewarriorCmd() {
875
1340
  console.log("Timewarrior: enabled");
876
1341
  console.log(" Parallel tracking: use `task start <id> --parallel`");
877
1342
  }
878
- const hookExists = fs3.existsSync(STANDARD_TIMEW_HOOK);
879
- const hookDisabled = fs3.existsSync(STANDARD_TIMEW_HOOK + ".disabled");
1343
+ const hookExists = fs5.existsSync(STANDARD_TIMEW_HOOK);
1344
+ const hookDisabled = fs5.existsSync(STANDARD_TIMEW_HOOK + ".disabled");
880
1345
  if (hookExists) {
881
1346
  console.log(`Standard hook: active (${STANDARD_TIMEW_HOOK})`);
882
1347
  if (tw?.enabled) {
@@ -894,9 +1359,9 @@ async function timewarriorCmd() {
894
1359
  const configPath = saveConfig(config);
895
1360
  console.log("Timewarrior tracking enabled");
896
1361
  console.log(`Config: ${configPath}`);
897
- if (fs3.existsSync(STANDARD_TIMEW_HOOK)) {
1362
+ if (fs5.existsSync(STANDARD_TIMEW_HOOK)) {
898
1363
  const disabled = STANDARD_TIMEW_HOOK + ".disabled";
899
- fs3.renameSync(STANDARD_TIMEW_HOOK, disabled);
1364
+ fs5.renameSync(STANDARD_TIMEW_HOOK, disabled);
900
1365
  console.log(`
901
1366
  Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
902
1367
  console.log("tw-bridge will handle Timewarrior tracking directly.");
@@ -909,8 +1374,8 @@ Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
909
1374
  console.log("Timewarrior tracking disabled");
910
1375
  console.log(`Config: ${configPath}`);
911
1376
  const disabled = STANDARD_TIMEW_HOOK + ".disabled";
912
- if (fs3.existsSync(disabled)) {
913
- fs3.renameSync(disabled, STANDARD_TIMEW_HOOK);
1377
+ if (fs5.existsSync(disabled)) {
1378
+ fs5.renameSync(disabled, STANDARD_TIMEW_HOOK);
914
1379
  console.log(`
915
1380
  Restored standard hook: ${STANDARD_TIMEW_HOOK}`);
916
1381
  }
@@ -940,28 +1405,28 @@ task() {
940
1405
  var SHELL_MARKER = "# tw-bridge: auto-context task wrapper";
941
1406
  function installShellFunction() {
942
1407
  const shell = process.env.SHELL ?? "/bin/bash";
943
- const home = os3.homedir();
1408
+ const home = os5.homedir();
944
1409
  let rcFile;
945
1410
  if (shell.endsWith("zsh")) {
946
- rcFile = path4.join(home, ".zshrc");
1411
+ rcFile = path6.join(home, ".zshrc");
947
1412
  } else {
948
- rcFile = path4.join(home, ".bashrc");
1413
+ rcFile = path6.join(home, ".bashrc");
949
1414
  }
950
- const existing = fs3.existsSync(rcFile) ? fs3.readFileSync(rcFile, "utf-8") : "";
1415
+ const existing = fs5.existsSync(rcFile) ? fs5.readFileSync(rcFile, "utf-8") : "";
951
1416
  if (existing.includes(SHELL_MARKER)) {
952
1417
  console.log(`
953
1418
  Shell integration already installed in ${rcFile}`);
954
1419
  return;
955
1420
  }
956
- fs3.appendFileSync(rcFile, "\n" + SHELL_FUNCTION + "\n");
1421
+ fs5.appendFileSync(rcFile, "\n" + SHELL_FUNCTION + "\n");
957
1422
  console.log(`
958
1423
  Shell integration installed in ${rcFile}`);
959
1424
  console.log("Restart your shell or run: source " + rcFile);
960
1425
  }
961
- var SEEN_FILE = path4.join(os3.homedir(), ".config", "tw-bridge", ".seen-dirs");
1426
+ var SEEN_FILE = path6.join(os5.homedir(), ".config", "tw-bridge", ".seen-dirs");
962
1427
  function loadSeenDirs() {
963
1428
  try {
964
- const raw = fs3.readFileSync(SEEN_FILE, "utf-8");
1429
+ const raw = fs5.readFileSync(SEEN_FILE, "utf-8");
965
1430
  return new Set(raw.split("\n").filter(Boolean));
966
1431
  } catch {
967
1432
  return /* @__PURE__ */ new Set();
@@ -971,15 +1436,15 @@ function markDirSeen(dir) {
971
1436
  const seen = loadSeenDirs();
972
1437
  if (seen.has(dir)) return;
973
1438
  seen.add(dir);
974
- fs3.mkdirSync(path4.dirname(SEEN_FILE), { recursive: true });
975
- fs3.writeFileSync(SEEN_FILE, [...seen].join("\n") + "\n");
1439
+ fs5.mkdirSync(path6.dirname(SEEN_FILE), { recursive: true });
1440
+ fs5.writeFileSync(SEEN_FILE, [...seen].join("\n") + "\n");
976
1441
  }
977
1442
  function isGitRepo(dir) {
978
1443
  try {
979
1444
  let current = dir;
980
- while (current !== path4.dirname(current)) {
981
- if (fs3.existsSync(path4.join(current, ".git"))) return true;
982
- current = path4.dirname(current);
1445
+ while (current !== path6.dirname(current)) {
1446
+ if (fs5.existsSync(path6.join(current, ".git"))) return true;
1447
+ current = path6.dirname(current);
983
1448
  }
984
1449
  return false;
985
1450
  } catch {
@@ -992,8 +1457,8 @@ async function which() {
992
1457
  for (const [_name, backend] of Object.entries(config.backends)) {
993
1458
  const backendCwd = backend.config?.cwd;
994
1459
  if (!backendCwd) continue;
995
- const resolved = path4.resolve(backendCwd);
996
- if (cwd === resolved || cwd.startsWith(resolved + path4.sep)) {
1460
+ const resolved = path6.resolve(backendCwd);
1461
+ if (cwd === resolved || cwd.startsWith(resolved + path6.sep)) {
997
1462
  const contextTag = backend.match.tags?.[0];
998
1463
  if (contextTag) {
999
1464
  process.stdout.write(contextTag);
@@ -1009,15 +1474,15 @@ async function which() {
1009
1474
  const seen = loadSeenDirs();
1010
1475
  let gitRoot = cwd;
1011
1476
  let current = cwd;
1012
- while (current !== path4.dirname(current)) {
1013
- if (fs3.existsSync(path4.join(current, ".git"))) {
1477
+ while (current !== path6.dirname(current)) {
1478
+ if (fs5.existsSync(path6.join(current, ".git"))) {
1014
1479
  gitRoot = current;
1015
1480
  break;
1016
1481
  }
1017
- current = path4.dirname(current);
1482
+ current = path6.dirname(current);
1018
1483
  }
1019
1484
  if (!seen.has(gitRoot)) {
1020
- const dirName = path4.basename(gitRoot);
1485
+ const dirName = path6.basename(gitRoot);
1021
1486
  process.stderr.write(
1022
1487
  `tw-bridge: unconfigured project "${dirName}". Run: tw-bridge add ${dirName} --adapter ghp
1023
1488
  `
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/hooks/on-modify.ts
4
- import fs3 from "fs";
5
- import path4 from "path";
6
- import os3 from "os";
7
- import { spawnSync as spawnSync2 } from "child_process";
4
+ import fs5 from "fs";
5
+ import { spawnSync as spawnSync3 } from "child_process";
8
6
 
9
7
  // src/config.ts
10
8
  import fs from "fs";
@@ -236,8 +234,8 @@ async function oauthLogin(clientId, clientSecret, rl) {
236
234
  saveTokens(tokens);
237
235
  return tokens;
238
236
  }
239
- async function asanaFetch(path5, token, options = {}) {
240
- const url = `${API_BASE}${path5}`;
237
+ async function asanaFetch(path6, token, options = {}) {
238
+ const url = `${API_BASE}${path6}`;
241
239
  const res = await fetch(url, {
242
240
  ...options,
243
241
  headers: {
@@ -253,9 +251,9 @@ async function asanaFetch(path5, token, options = {}) {
253
251
  const json = await res.json();
254
252
  return json.data;
255
253
  }
256
- async function asanaFetchAll(path5, token) {
254
+ async function asanaFetchAll(path6, token) {
257
255
  const results = [];
258
- let nextPage = path5;
256
+ let nextPage = path6;
259
257
  while (nextPage) {
260
258
  const url = `${API_BASE}${nextPage}`;
261
259
  const res = await fetch(url, {
@@ -567,10 +565,319 @@ function mapAsanaPriority(fields) {
567
565
  return void 0;
568
566
  }
569
567
 
568
+ // src/adapters/strety.ts
569
+ import fs3 from "fs";
570
+ import path4 from "path";
571
+ import os3 from "os";
572
+ import { execSync as execSync2 } from "child_process";
573
+ import { createInterface as createInterface2 } from "readline/promises";
574
+ import { stdin as stdin2, stdout as stdout2 } from "process";
575
+ var API_BASE2 = "https://2.strety.com/api/v1";
576
+ var OAUTH_AUTHORIZE2 = `${API_BASE2}/oauth/authorize`;
577
+ var OAUTH_TOKEN2 = `${API_BASE2}/oauth/token`;
578
+ var TOKEN_FILE2 = path4.join(os3.homedir(), ".config", "tw-bridge", "strety-tokens.json");
579
+ function loadTokens2() {
580
+ try {
581
+ return JSON.parse(fs3.readFileSync(TOKEN_FILE2, "utf-8"));
582
+ } catch {
583
+ return null;
584
+ }
585
+ }
586
+ function saveTokens2(tokens) {
587
+ fs3.mkdirSync(path4.dirname(TOKEN_FILE2), { recursive: true });
588
+ fs3.writeFileSync(TOKEN_FILE2, JSON.stringify(tokens, null, 2), { mode: 384 });
589
+ }
590
+ async function refreshAccessToken2(tokens) {
591
+ const now = Date.now();
592
+ if (tokens.access_token && tokens.expires_at > now + 3e5) {
593
+ return tokens.access_token;
594
+ }
595
+ const res = await fetch(OAUTH_TOKEN2, {
596
+ method: "POST",
597
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
598
+ body: new URLSearchParams({
599
+ grant_type: "refresh_token",
600
+ refresh_token: tokens.refresh_token,
601
+ client_id: tokens.client_id,
602
+ client_secret: tokens.client_secret
603
+ })
604
+ });
605
+ if (!res.ok) {
606
+ const body = await res.text();
607
+ throw new Error(`Token refresh failed (${res.status}): ${body}`);
608
+ }
609
+ const data = await res.json();
610
+ tokens.access_token = data.access_token;
611
+ if (data.refresh_token) {
612
+ tokens.refresh_token = data.refresh_token;
613
+ }
614
+ tokens.expires_at = now + (data.expires_in ?? 7200) * 1e3;
615
+ saveTokens2(tokens);
616
+ return tokens.access_token;
617
+ }
618
+ var REDIRECT_URI = "https://localhost/callback";
619
+ async function oauthLogin2(clientId, clientSecret, rl) {
620
+ const authUrl = `${OAUTH_AUTHORIZE2}?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=${encodeURIComponent("read write")}`;
621
+ console.log(`
622
+ Opening browser for authorization...`);
623
+ console.log(` If it doesn't open, visit:
624
+ ${authUrl}
625
+ `);
626
+ try {
627
+ execSync2(`xdg-open "${authUrl}" 2>/dev/null || open "${authUrl}" 2>/dev/null`, {
628
+ stdio: "ignore"
629
+ });
630
+ } catch {
631
+ }
632
+ console.log(" After authorizing, the browser will redirect to a page that");
633
+ console.log(" won't load. Copy the full URL from the address bar and paste it here.\n");
634
+ const pasted = (await rl.question("Paste the callback URL: ")).trim();
635
+ let code;
636
+ try {
637
+ const url = new URL(pasted);
638
+ const error = url.searchParams.get("error");
639
+ if (error) {
640
+ throw new Error(`OAuth error: ${error}`);
641
+ }
642
+ const authCode = url.searchParams.get("code");
643
+ if (!authCode) {
644
+ throw new Error("No authorization code found in URL");
645
+ }
646
+ code = authCode;
647
+ } catch (err) {
648
+ if (err instanceof Error && err.message.startsWith("OAuth error:")) throw err;
649
+ if (err instanceof Error && err.message.startsWith("No authorization")) throw err;
650
+ throw new Error(`Could not parse callback URL: ${pasted}`);
651
+ }
652
+ const tokenRes = await fetch(OAUTH_TOKEN2, {
653
+ method: "POST",
654
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
655
+ body: new URLSearchParams({
656
+ grant_type: "authorization_code",
657
+ code,
658
+ redirect_uri: REDIRECT_URI,
659
+ client_id: clientId,
660
+ client_secret: clientSecret
661
+ })
662
+ });
663
+ if (!tokenRes.ok) {
664
+ const body = await tokenRes.text();
665
+ throw new Error(`Token exchange failed (${tokenRes.status}): ${body}`);
666
+ }
667
+ const data = await tokenRes.json();
668
+ const tokens = {
669
+ access_token: data.access_token,
670
+ refresh_token: data.refresh_token,
671
+ expires_at: Date.now() + (data.expires_in ?? 7200) * 1e3,
672
+ client_id: clientId,
673
+ client_secret: clientSecret
674
+ };
675
+ saveTokens2(tokens);
676
+ return tokens;
677
+ }
678
+ async function stretyFetch(urlOrPath, token, options = {}) {
679
+ const url = urlOrPath.startsWith("http") ? urlOrPath : `${API_BASE2}${urlOrPath}`;
680
+ const res = await fetch(url, {
681
+ ...options,
682
+ headers: {
683
+ Authorization: `Bearer ${token}`,
684
+ Accept: "application/vnd.api+json",
685
+ "Content-Type": "application/vnd.api+json",
686
+ ...options.headers
687
+ }
688
+ });
689
+ if (!res.ok) {
690
+ const body = await res.text();
691
+ throw new Error(`Strety API ${res.status}: ${body}`);
692
+ }
693
+ return res.json();
694
+ }
695
+ async function stretyFetchAll(path6, token) {
696
+ const results = [];
697
+ let url = `${API_BASE2}${path6}`;
698
+ while (url) {
699
+ const json = await stretyFetch(url, token);
700
+ results.push(...json.data);
701
+ url = json.links?.next ?? null;
702
+ }
703
+ return results;
704
+ }
705
+ async function pickOne2(rl, label, items) {
706
+ console.log(`
707
+ ${label}:`);
708
+ for (let i = 0; i < items.length; i++) {
709
+ const name = items[i].attributes.name ?? items[i].attributes.title ?? items[i].id;
710
+ console.log(` ${i + 1}) ${name}`);
711
+ }
712
+ while (true) {
713
+ const answer = await rl.question(`Choose (1-${items.length}): `);
714
+ const idx = parseInt(answer, 10) - 1;
715
+ if (idx >= 0 && idx < items.length) {
716
+ const item = items[idx];
717
+ return { id: item.id, name: item.attributes.name ?? item.attributes.title ?? item.id };
718
+ }
719
+ console.log(" Invalid choice, try again.");
720
+ }
721
+ }
722
+ var StretyAdapter = class {
723
+ name = "strety";
724
+ config;
725
+ accessToken = null;
726
+ async resolveToken() {
727
+ const auth = this.config.auth;
728
+ if (auth === "oauth") {
729
+ const tokens = loadTokens2();
730
+ if (!tokens) throw new Error("No OAuth tokens found. Run `tw-bridge add` to authenticate.");
731
+ this.accessToken = await refreshAccessToken2(tokens);
732
+ return this.accessToken;
733
+ }
734
+ if (this.accessToken) return this.accessToken;
735
+ if (auth.startsWith("env:")) {
736
+ const envVar = auth.slice(4);
737
+ const val = process.env[envVar];
738
+ if (!val) throw new Error(`Environment variable ${envVar} not set`);
739
+ this.accessToken = val;
740
+ return val;
741
+ }
742
+ this.accessToken = auth;
743
+ return auth;
744
+ }
745
+ defaultConfig(_cwd) {
746
+ return {
747
+ auth: "env:STRETY_TOKEN",
748
+ assignee_id: "",
749
+ assignee_name: ""
750
+ };
751
+ }
752
+ async setup(_cwd) {
753
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
754
+ try {
755
+ console.log("\n\u2500\u2500 Strety Adapter Setup \u2500\u2500\n");
756
+ console.log("Authentication:");
757
+ console.log(" 1) OAuth (browser login)");
758
+ console.log(" 2) Environment variable");
759
+ const authChoice = (await rl.question("\nChoose (1-2): ")).trim();
760
+ let authConfig;
761
+ let token;
762
+ if (authChoice === "1") {
763
+ console.log(`
764
+ Register a Strety app at My Integrations > My Apps.`);
765
+ console.log(` Set the Redirect URI to: ${REDIRECT_URI}
766
+ `);
767
+ const clientId = (await rl.question("Strety App Client ID: ")).trim();
768
+ const clientSecret = (await rl.question("Strety App Client Secret: ")).trim();
769
+ if (!clientId || !clientSecret) {
770
+ console.error("\n Client ID and Secret are required.");
771
+ rl.close();
772
+ process.exit(1);
773
+ }
774
+ const tokens = await oauthLogin2(clientId, clientSecret, rl);
775
+ token = tokens.access_token;
776
+ authConfig = "oauth";
777
+ } else {
778
+ const envVar = await rl.question("Environment variable name [STRETY_TOKEN]: ");
779
+ const varName = envVar.trim() || "STRETY_TOKEN";
780
+ authConfig = `env:${varName}`;
781
+ token = process.env[varName] ?? "";
782
+ if (!token) {
783
+ console.error(`
784
+ Cannot complete setup without a valid token.`);
785
+ console.error(` Set ${varName} in your environment and try again.`);
786
+ rl.close();
787
+ process.exit(1);
788
+ }
789
+ }
790
+ console.log("\nVerifying...");
791
+ let people;
792
+ try {
793
+ people = await stretyFetchAll("/people?page[size]=20", token);
794
+ } catch (err) {
795
+ const msg = err instanceof Error ? err.message : String(err);
796
+ console.error(` Authentication failed: ${msg}`);
797
+ rl.close();
798
+ process.exit(1);
799
+ }
800
+ console.log(` Connected! Found ${people.length} people.`);
801
+ const person = await pickOne2(rl, "Which person are you? (for filtering your todos)", people);
802
+ rl.close();
803
+ console.log(`
804
+ Assignee: ${person.name}`);
805
+ console.log(" Syncing: Todos assigned to you");
806
+ return {
807
+ auth: authConfig,
808
+ assignee_id: person.id,
809
+ assignee_name: person.name
810
+ };
811
+ } catch (err) {
812
+ rl.close();
813
+ throw err;
814
+ }
815
+ }
816
+ async init(config) {
817
+ this.config = config;
818
+ if (!this.config.assignee_id) {
819
+ throw new Error('strety adapter requires "assignee_id" in config');
820
+ }
821
+ }
822
+ async pull() {
823
+ const token = await this.resolveToken();
824
+ const raw = await stretyFetchAll(
825
+ `/todos?filter[assignee_id]=${this.config.assignee_id}&page[size]=20`,
826
+ token
827
+ );
828
+ const tasks = [];
829
+ for (const item of raw) {
830
+ const attrs = item.attributes;
831
+ if (attrs.completed_at) continue;
832
+ const priority = mapStretyPriority(attrs.priority);
833
+ const task = {
834
+ uuid: "",
835
+ description: attrs.title,
836
+ status: "pending",
837
+ entry: attrs.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
838
+ tags: [],
839
+ priority,
840
+ backend: "",
841
+ backend_id: item.id,
842
+ ...attrs.due_date && { due: new Date(attrs.due_date).toISOString() }
843
+ };
844
+ tasks.push(task);
845
+ }
846
+ return tasks;
847
+ }
848
+ async onDone(task) {
849
+ const todoId = task.backend_id;
850
+ if (!todoId) return;
851
+ const token = await this.resolveToken();
852
+ process.stderr.write(`tw-bridge [strety]: marking todo complete
853
+ `);
854
+ await stretyFetch(`/todos/${todoId}`, token, {
855
+ method: "PATCH",
856
+ headers: { "If-Match": "*" },
857
+ body: JSON.stringify({
858
+ data: {
859
+ type: "todo",
860
+ attributes: {
861
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
862
+ }
863
+ }
864
+ })
865
+ });
866
+ }
867
+ };
868
+ function mapStretyPriority(priority) {
869
+ if (!priority || priority === "none") return void 0;
870
+ if (priority === "highest" || priority === "high") return "H";
871
+ if (priority === "medium") return "M";
872
+ if (priority === "low" || priority === "lowest") return "L";
873
+ return void 0;
874
+ }
875
+
570
876
  // src/registry.ts
571
877
  var BUILTIN_ADAPTERS = {
572
878
  ghp: () => new GhpAdapter(),
573
- asana: () => new AsanaAdapter()
879
+ asana: () => new AsanaAdapter(),
880
+ strety: () => new StretyAdapter()
574
881
  };
575
882
  function matchBackend(task, config) {
576
883
  if (task.backend && config.backends[task.backend]) {
@@ -601,18 +908,22 @@ async function resolveAdapter(task, config) {
601
908
  return adapter;
602
909
  }
603
910
 
604
- // src/hooks/on-modify.ts
605
- var TRACKING_FILE = path4.join(os3.homedir(), ".config", "tw-bridge", "tracking.json");
911
+ // src/tracking.ts
912
+ import fs4 from "fs";
913
+ import path5 from "path";
914
+ import os4 from "os";
915
+ import { spawnSync as spawnSync2 } from "child_process";
916
+ var TRACKING_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", "tracking.json");
606
917
  function loadTracking() {
607
918
  try {
608
- return JSON.parse(fs3.readFileSync(TRACKING_FILE, "utf-8"));
919
+ return JSON.parse(fs4.readFileSync(TRACKING_FILE, "utf-8"));
609
920
  } catch {
610
921
  return {};
611
922
  }
612
923
  }
613
924
  function saveTracking(state) {
614
- fs3.mkdirSync(path4.dirname(TRACKING_FILE), { recursive: true });
615
- fs3.writeFileSync(TRACKING_FILE, JSON.stringify(state));
925
+ fs4.mkdirSync(path5.dirname(TRACKING_FILE), { recursive: true });
926
+ fs4.writeFileSync(TRACKING_FILE, JSON.stringify(state));
616
927
  }
617
928
  function mergedTags(state) {
618
929
  const all = /* @__PURE__ */ new Set();
@@ -621,19 +932,6 @@ function mergedTags(state) {
621
932
  }
622
933
  return [...all];
623
934
  }
624
- function timewTags(task) {
625
- const tags = [];
626
- if (task.backend) tags.push(task.backend);
627
- if (task.backend_id) tags.push(`#${task.backend_id}`);
628
- if (task.project) tags.push(task.project);
629
- for (const tag of task.tags ?? []) {
630
- if (!tag.includes("_")) tags.push(tag);
631
- }
632
- return [...new Set(tags)];
633
- }
634
- function taskKey(task) {
635
- return task.backend_id ? `#${task.backend_id}` : task.uuid;
636
- }
637
935
  function getCurrentInterval() {
638
936
  const result = spawnSync2("timew", ["export"], {
639
937
  encoding: "utf-8",
@@ -647,6 +945,21 @@ function getCurrentInterval() {
647
945
  return null;
648
946
  }
649
947
  }
948
+
949
+ // src/hooks/on-modify.ts
950
+ function timewTags(task) {
951
+ const tags = [];
952
+ if (task.backend) tags.push(task.backend);
953
+ if (task.backend_id) tags.push(`#${task.backend_id}`);
954
+ if (task.project) tags.push(task.project);
955
+ for (const tag of task.tags ?? []) {
956
+ if (!tag.includes("_")) tags.push(tag);
957
+ }
958
+ return [...new Set(tags)];
959
+ }
960
+ function taskKey(task) {
961
+ return task.backend_id ? `#${task.backend_id}` : task.uuid;
962
+ }
650
963
  function formatDuration(isoStart) {
651
964
  const start = new Date(
652
965
  isoStart.replace(
@@ -685,10 +998,10 @@ tw-bridge: Currently tracking: ${tags} (${duration})`
685
998
  lines.push(" [s] Switch \u2014 stop current, start new task (default)");
686
999
  lines.push(" [p] Parallel \u2014 add new task to current tracking");
687
1000
  lines.push("");
688
- fs3.writeSync(ttyFd, lines.join("\n"));
689
- fs3.writeSync(ttyFd, " > ");
1001
+ fs5.writeSync(ttyFd, lines.join("\n"));
1002
+ fs5.writeSync(ttyFd, " > ");
690
1003
  const buf = Buffer.alloc(64);
691
- const bytesRead = fs3.readSync(ttyFd, buf, 0, 64, null);
1004
+ const bytesRead = fs5.readSync(ttyFd, buf, 0, 64, null);
692
1005
  const answer = buf.toString("utf-8", 0, bytesRead).trim().toLowerCase();
693
1006
  return answer.startsWith("p") ? "parallel" : "switch";
694
1007
  }
@@ -713,12 +1026,12 @@ function handleTimewarriorStart(config, newTask, ttyFd) {
713
1026
  tracking[key] = newTags;
714
1027
  saveTracking(tracking);
715
1028
  const merged = mergedTags(tracking);
716
- spawnSync2("timew", ["stop"], { stdio: "pipe" });
717
- spawnSync2("timew", ["start", ...merged], { stdio: "pipe" });
1029
+ spawnSync3("timew", ["stop"], { stdio: "pipe" });
1030
+ spawnSync3("timew", ["start", ...merged], { stdio: "pipe" });
718
1031
  } else {
719
1032
  saveTracking({ [key]: newTags });
720
- spawnSync2("timew", ["stop"], { stdio: "pipe" });
721
- spawnSync2("timew", ["start", ...newTags], { stdio: "pipe" });
1033
+ spawnSync3("timew", ["stop"], { stdio: "pipe" });
1034
+ spawnSync3("timew", ["start", ...newTags], { stdio: "pipe" });
722
1035
  }
723
1036
  }
724
1037
  function handleTimewarriorStop(config, task) {
@@ -728,13 +1041,13 @@ function handleTimewarriorStop(config, task) {
728
1041
  delete tracking[key];
729
1042
  saveTracking(tracking);
730
1043
  const remaining = mergedTags(tracking);
731
- spawnSync2("timew", ["stop"], { stdio: "pipe" });
1044
+ spawnSync3("timew", ["stop"], { stdio: "pipe" });
732
1045
  if (remaining.length > 0) {
733
- spawnSync2("timew", ["start", ...remaining], { stdio: "pipe" });
1046
+ spawnSync3("timew", ["start", ...remaining], { stdio: "pipe" });
734
1047
  }
735
1048
  }
736
1049
  async function main() {
737
- const input = fs3.readFileSync("/dev/stdin", "utf-8").trim().split("\n");
1050
+ const input = fs5.readFileSync("/dev/stdin", "utf-8").trim().split("\n");
738
1051
  const oldTask = JSON.parse(input[0]);
739
1052
  const newTask = JSON.parse(input[1]);
740
1053
  process.stdout.write(JSON.stringify(newTask) + "\n");
@@ -745,7 +1058,7 @@ async function main() {
745
1058
  let ttyFd = null;
746
1059
  if (wasStarted) {
747
1060
  try {
748
- ttyFd = fs3.openSync("/dev/tty", "r+");
1061
+ ttyFd = fs5.openSync("/dev/tty", "r+");
749
1062
  } catch {
750
1063
  }
751
1064
  }
@@ -773,7 +1086,7 @@ async function main() {
773
1086
  process.stderr.write(`tw-bridge: ${msg}
774
1087
  `);
775
1088
  } finally {
776
- if (ttyFd !== null) fs3.closeSync(ttyFd);
1089
+ if (ttyFd !== null) fs5.closeSync(ttyFd);
777
1090
  }
778
1091
  }
779
1092
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bretwardjames/tw-bridge",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Taskwarrior backend bridge — unified sync and hooks for multiple task management platforms",
5
5
  "type": "module",
6
6
  "license": "MIT",