@bretwardjames/tw-bridge 0.5.0 → 0.7.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 fs4 from "fs";
5
- import path5 from "path";
6
- import os4 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(path6, token, options = {}) {
245
- const url = `${API_BASE}${path6}`;
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(path6, token, options = {}) {
258
258
  const json = await res.json();
259
259
  return json.data;
260
260
  }
261
- async function asanaFetchAll(path6, token) {
261
+ async function asanaFetchAll(path7, token) {
262
262
  const results = [];
263
- let nextPage = path6;
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]) {
@@ -697,6 +1006,34 @@ function completeTask(existing, keepTags) {
697
1006
  );
698
1007
  return result.status === 0;
699
1008
  }
1009
+ function findTaskByBackendId(backend, backendId) {
1010
+ const existing = getExistingTasks(backend);
1011
+ return existing.get(backendId) ?? null;
1012
+ }
1013
+ function startTask(uuid) {
1014
+ const result = spawnSync2(
1015
+ "task",
1016
+ ["rc.confirmation=off", uuid, "start"],
1017
+ {
1018
+ encoding: "utf-8",
1019
+ stdio: ["pipe", "pipe", "pipe"],
1020
+ env: { ...process.env, TW_BRIDGE_REVERSE_SYNC: "1" }
1021
+ }
1022
+ );
1023
+ return result.status === 0;
1024
+ }
1025
+ function doneTask(uuid) {
1026
+ const result = spawnSync2(
1027
+ "task",
1028
+ ["rc.confirmation=off", uuid, "done"],
1029
+ {
1030
+ encoding: "utf-8",
1031
+ stdio: ["pipe", "pipe", "pipe"],
1032
+ env: { ...process.env, TW_BRIDGE_REVERSE_SYNC: "1" }
1033
+ }
1034
+ );
1035
+ return result.status === 0;
1036
+ }
700
1037
  function updateTaskDescription(existing, newDescription) {
701
1038
  if (existing.description === newDescription) return false;
702
1039
  const result = spawnSync2(
@@ -708,21 +1045,21 @@ function updateTaskDescription(existing, newDescription) {
708
1045
  }
709
1046
 
710
1047
  // src/tracking.ts
711
- import fs3 from "fs";
712
- import path4 from "path";
713
- import os3 from "os";
1048
+ import fs4 from "fs";
1049
+ import path5 from "path";
1050
+ import os4 from "os";
714
1051
  import { spawnSync as spawnSync3 } from "child_process";
715
- var TRACKING_FILE = path4.join(os3.homedir(), ".config", "tw-bridge", "tracking.json");
1052
+ var TRACKING_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", "tracking.json");
716
1053
  function loadTracking() {
717
1054
  try {
718
- return JSON.parse(fs3.readFileSync(TRACKING_FILE, "utf-8"));
1055
+ return JSON.parse(fs4.readFileSync(TRACKING_FILE, "utf-8"));
719
1056
  } catch {
720
1057
  return {};
721
1058
  }
722
1059
  }
723
1060
  function saveTracking(state) {
724
- fs3.mkdirSync(path4.dirname(TRACKING_FILE), { recursive: true });
725
- fs3.writeFileSync(TRACKING_FILE, JSON.stringify(state));
1061
+ fs4.mkdirSync(path5.dirname(TRACKING_FILE), { recursive: true });
1062
+ fs4.writeFileSync(TRACKING_FILE, JSON.stringify(state));
726
1063
  }
727
1064
  function mergedTags(state) {
728
1065
  const all = /* @__PURE__ */ new Set();
@@ -760,11 +1097,13 @@ function getActiveMeetings() {
760
1097
  }
761
1098
 
762
1099
  // src/cli.ts
763
- var HOOKS_DIR = path5.join(os4.homedir(), ".task", "hooks");
1100
+ var HOOKS_DIR = path6.join(os5.homedir(), ".task", "hooks");
764
1101
  var commands = {
765
1102
  add: addBackend,
766
1103
  install,
767
1104
  sync,
1105
+ start: startCmd,
1106
+ done: doneCmd,
768
1107
  meeting: meetingCmd,
769
1108
  timewarrior: timewarriorCmd,
770
1109
  which,
@@ -778,6 +1117,8 @@ async function main() {
778
1117
  console.log(" add Add a new backend instance");
779
1118
  console.log(" install Install Taskwarrior hooks and shell integration");
780
1119
  console.log(" sync Pull tasks from all backends");
1120
+ console.log(" start Start a task by backend ID (e.g., tw-bridge start ghp#123)");
1121
+ console.log(" done Complete a task by backend ID (e.g., tw-bridge done ghp#123)");
781
1122
  console.log(" meeting Track meetings in Timewarrior (no task created)");
782
1123
  console.log(" timewarrior Manage Timewarrior integration");
783
1124
  console.log(" which Print the context for the current directory");
@@ -854,18 +1195,18 @@ Added backend "${name}" (adapter: ${adapterType})`);
854
1195
  Use 'task context ${matchTag}' to switch to this project.`);
855
1196
  }
856
1197
  async function install() {
857
- fs4.mkdirSync(HOOKS_DIR, { recursive: true });
858
- const hookSource = path5.resolve(
859
- path5.dirname(new URL(import.meta.url).pathname),
1198
+ fs5.mkdirSync(HOOKS_DIR, { recursive: true });
1199
+ const hookSource = path6.resolve(
1200
+ path6.dirname(new URL(import.meta.url).pathname),
860
1201
  "hooks",
861
1202
  "on-modify.js"
862
1203
  );
863
- const hookTarget = path5.join(HOOKS_DIR, "on-modify.tw-bridge");
864
- if (fs4.existsSync(hookTarget)) {
865
- fs4.unlinkSync(hookTarget);
1204
+ const hookTarget = path6.join(HOOKS_DIR, "on-modify.tw-bridge");
1205
+ if (fs5.existsSync(hookTarget)) {
1206
+ fs5.unlinkSync(hookTarget);
866
1207
  }
867
- fs4.symlinkSync(hookSource, hookTarget);
868
- fs4.chmodSync(hookSource, 493);
1208
+ fs5.symlinkSync(hookSource, hookTarget);
1209
+ fs5.chmodSync(hookSource, 493);
869
1210
  console.log(`Installed hook: ${hookTarget} -> ${hookSource}`);
870
1211
  console.log("\nAdd these to your .taskrc:\n");
871
1212
  console.log("# --- tw-bridge UDAs ---");
@@ -882,26 +1223,26 @@ async function install() {
882
1223
  console.log("urgency.user.tag.ready_for_beta.coefficient=-4.0");
883
1224
  console.log("urgency.user.tag.in_beta.coefficient=-6.0");
884
1225
  installTimewExtension();
885
- if (fs4.existsSync(STANDARD_TIMEW_HOOK)) {
1226
+ if (fs5.existsSync(STANDARD_TIMEW_HOOK)) {
886
1227
  console.log("\nTimewarrior hook detected. To enable tw-bridge time tracking:");
887
1228
  console.log(" tw-bridge timewarrior enable");
888
1229
  }
889
1230
  installShellFunction();
890
1231
  }
891
- var TIMEW_EXT_DIR = path5.join(os4.homedir(), ".timewarrior", "extensions");
1232
+ var TIMEW_EXT_DIR = path6.join(os5.homedir(), ".timewarrior", "extensions");
892
1233
  function installTimewExtension() {
893
- fs4.mkdirSync(TIMEW_EXT_DIR, { recursive: true });
894
- const extSource = path5.resolve(
895
- path5.dirname(new URL(import.meta.url).pathname),
1234
+ fs5.mkdirSync(TIMEW_EXT_DIR, { recursive: true });
1235
+ const extSource = path6.resolve(
1236
+ path6.dirname(new URL(import.meta.url).pathname),
896
1237
  "extensions",
897
1238
  "bridge.js"
898
1239
  );
899
- const extTarget = path5.join(TIMEW_EXT_DIR, "bridge");
900
- if (fs4.existsSync(extTarget)) {
901
- fs4.unlinkSync(extTarget);
1240
+ const extTarget = path6.join(TIMEW_EXT_DIR, "bridge");
1241
+ if (fs5.existsSync(extTarget)) {
1242
+ fs5.unlinkSync(extTarget);
902
1243
  }
903
- fs4.symlinkSync(extSource, extTarget);
904
- fs4.chmodSync(extSource, 493);
1244
+ fs5.symlinkSync(extSource, extTarget);
1245
+ fs5.chmodSync(extSource, 493);
905
1246
  console.log(`
906
1247
  Installed Timewarrior extension: ${extTarget} -> ${extSource}`);
907
1248
  console.log(" Usage: timew bridge [task-time|wall-time] [project-filter]");
@@ -915,8 +1256,8 @@ function detectProjectContext() {
915
1256
  for (const [, backend] of Object.entries(config.backends)) {
916
1257
  const backendCwd = backend.config?.cwd;
917
1258
  if (!backendCwd) continue;
918
- const resolved = path5.resolve(backendCwd);
919
- if (cwd === resolved || cwd.startsWith(resolved + path5.sep)) {
1259
+ const resolved = path6.resolve(backendCwd);
1260
+ if (cwd === resolved || cwd.startsWith(resolved + path6.sep)) {
920
1261
  return backend.match.tags?.[0] ?? null;
921
1262
  }
922
1263
  }
@@ -1008,7 +1349,7 @@ async function meetingCmd() {
1008
1349
  console.error(`Unknown subcommand: ${sub}`);
1009
1350
  process.exit(1);
1010
1351
  }
1011
- var STANDARD_TIMEW_HOOK = path5.join(HOOKS_DIR, "on-modify.timewarrior");
1352
+ var STANDARD_TIMEW_HOOK = path6.join(HOOKS_DIR, "on-modify.timewarrior");
1012
1353
  async function timewarriorCmd() {
1013
1354
  const sub = process.argv[3];
1014
1355
  if (!sub || sub === "--help") {
@@ -1031,8 +1372,8 @@ async function timewarriorCmd() {
1031
1372
  console.log("Timewarrior: enabled");
1032
1373
  console.log(" Parallel tracking: use `task start <id> --parallel`");
1033
1374
  }
1034
- const hookExists = fs4.existsSync(STANDARD_TIMEW_HOOK);
1035
- const hookDisabled = fs4.existsSync(STANDARD_TIMEW_HOOK + ".disabled");
1375
+ const hookExists = fs5.existsSync(STANDARD_TIMEW_HOOK);
1376
+ const hookDisabled = fs5.existsSync(STANDARD_TIMEW_HOOK + ".disabled");
1036
1377
  if (hookExists) {
1037
1378
  console.log(`Standard hook: active (${STANDARD_TIMEW_HOOK})`);
1038
1379
  if (tw?.enabled) {
@@ -1050,9 +1391,9 @@ async function timewarriorCmd() {
1050
1391
  const configPath = saveConfig(config);
1051
1392
  console.log("Timewarrior tracking enabled");
1052
1393
  console.log(`Config: ${configPath}`);
1053
- if (fs4.existsSync(STANDARD_TIMEW_HOOK)) {
1394
+ if (fs5.existsSync(STANDARD_TIMEW_HOOK)) {
1054
1395
  const disabled = STANDARD_TIMEW_HOOK + ".disabled";
1055
- fs4.renameSync(STANDARD_TIMEW_HOOK, disabled);
1396
+ fs5.renameSync(STANDARD_TIMEW_HOOK, disabled);
1056
1397
  console.log(`
1057
1398
  Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
1058
1399
  console.log("tw-bridge will handle Timewarrior tracking directly.");
@@ -1065,8 +1406,8 @@ Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
1065
1406
  console.log("Timewarrior tracking disabled");
1066
1407
  console.log(`Config: ${configPath}`);
1067
1408
  const disabled = STANDARD_TIMEW_HOOK + ".disabled";
1068
- if (fs4.existsSync(disabled)) {
1069
- fs4.renameSync(disabled, STANDARD_TIMEW_HOOK);
1409
+ if (fs5.existsSync(disabled)) {
1410
+ fs5.renameSync(disabled, STANDARD_TIMEW_HOOK);
1070
1411
  console.log(`
1071
1412
  Restored standard hook: ${STANDARD_TIMEW_HOOK}`);
1072
1413
  }
@@ -1096,28 +1437,28 @@ task() {
1096
1437
  var SHELL_MARKER = "# tw-bridge: auto-context task wrapper";
1097
1438
  function installShellFunction() {
1098
1439
  const shell = process.env.SHELL ?? "/bin/bash";
1099
- const home = os4.homedir();
1440
+ const home = os5.homedir();
1100
1441
  let rcFile;
1101
1442
  if (shell.endsWith("zsh")) {
1102
- rcFile = path5.join(home, ".zshrc");
1443
+ rcFile = path6.join(home, ".zshrc");
1103
1444
  } else {
1104
- rcFile = path5.join(home, ".bashrc");
1445
+ rcFile = path6.join(home, ".bashrc");
1105
1446
  }
1106
- const existing = fs4.existsSync(rcFile) ? fs4.readFileSync(rcFile, "utf-8") : "";
1447
+ const existing = fs5.existsSync(rcFile) ? fs5.readFileSync(rcFile, "utf-8") : "";
1107
1448
  if (existing.includes(SHELL_MARKER)) {
1108
1449
  console.log(`
1109
1450
  Shell integration already installed in ${rcFile}`);
1110
1451
  return;
1111
1452
  }
1112
- fs4.appendFileSync(rcFile, "\n" + SHELL_FUNCTION + "\n");
1453
+ fs5.appendFileSync(rcFile, "\n" + SHELL_FUNCTION + "\n");
1113
1454
  console.log(`
1114
1455
  Shell integration installed in ${rcFile}`);
1115
1456
  console.log("Restart your shell or run: source " + rcFile);
1116
1457
  }
1117
- var SEEN_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", ".seen-dirs");
1458
+ var SEEN_FILE = path6.join(os5.homedir(), ".config", "tw-bridge", ".seen-dirs");
1118
1459
  function loadSeenDirs() {
1119
1460
  try {
1120
- const raw = fs4.readFileSync(SEEN_FILE, "utf-8");
1461
+ const raw = fs5.readFileSync(SEEN_FILE, "utf-8");
1121
1462
  return new Set(raw.split("\n").filter(Boolean));
1122
1463
  } catch {
1123
1464
  return /* @__PURE__ */ new Set();
@@ -1127,15 +1468,15 @@ function markDirSeen(dir) {
1127
1468
  const seen = loadSeenDirs();
1128
1469
  if (seen.has(dir)) return;
1129
1470
  seen.add(dir);
1130
- fs4.mkdirSync(path5.dirname(SEEN_FILE), { recursive: true });
1131
- fs4.writeFileSync(SEEN_FILE, [...seen].join("\n") + "\n");
1471
+ fs5.mkdirSync(path6.dirname(SEEN_FILE), { recursive: true });
1472
+ fs5.writeFileSync(SEEN_FILE, [...seen].join("\n") + "\n");
1132
1473
  }
1133
1474
  function isGitRepo(dir) {
1134
1475
  try {
1135
1476
  let current = dir;
1136
- while (current !== path5.dirname(current)) {
1137
- if (fs4.existsSync(path5.join(current, ".git"))) return true;
1138
- current = path5.dirname(current);
1477
+ while (current !== path6.dirname(current)) {
1478
+ if (fs5.existsSync(path6.join(current, ".git"))) return true;
1479
+ current = path6.dirname(current);
1139
1480
  }
1140
1481
  return false;
1141
1482
  } catch {
@@ -1148,8 +1489,8 @@ async function which() {
1148
1489
  for (const [_name, backend] of Object.entries(config.backends)) {
1149
1490
  const backendCwd = backend.config?.cwd;
1150
1491
  if (!backendCwd) continue;
1151
- const resolved = path5.resolve(backendCwd);
1152
- if (cwd === resolved || cwd.startsWith(resolved + path5.sep)) {
1492
+ const resolved = path6.resolve(backendCwd);
1493
+ if (cwd === resolved || cwd.startsWith(resolved + path6.sep)) {
1153
1494
  const contextTag = backend.match.tags?.[0];
1154
1495
  if (contextTag) {
1155
1496
  process.stdout.write(contextTag);
@@ -1165,15 +1506,15 @@ async function which() {
1165
1506
  const seen = loadSeenDirs();
1166
1507
  let gitRoot = cwd;
1167
1508
  let current = cwd;
1168
- while (current !== path5.dirname(current)) {
1169
- if (fs4.existsSync(path5.join(current, ".git"))) {
1509
+ while (current !== path6.dirname(current)) {
1510
+ if (fs5.existsSync(path6.join(current, ".git"))) {
1170
1511
  gitRoot = current;
1171
1512
  break;
1172
1513
  }
1173
- current = path5.dirname(current);
1514
+ current = path6.dirname(current);
1174
1515
  }
1175
1516
  if (!seen.has(gitRoot)) {
1176
- const dirName = path5.basename(gitRoot);
1517
+ const dirName = path6.basename(gitRoot);
1177
1518
  process.stderr.write(
1178
1519
  `tw-bridge: unconfigured project "${dirName}". Run: tw-bridge add ${dirName} --adapter ghp
1179
1520
  `
@@ -1193,6 +1534,75 @@ async function sync() {
1193
1534
  await syncBackend(name, config.backends[name], config);
1194
1535
  }
1195
1536
  }
1537
+ function parseBackendRef(ref) {
1538
+ const match = ref.match(/^([^#]+)#(.+)$/);
1539
+ if (!match) return null;
1540
+ return { backend: match[1], id: match[2] };
1541
+ }
1542
+ async function resolveTaskByRef(ref) {
1543
+ const config = loadConfig();
1544
+ const backendName = Object.keys(config.backends).find(
1545
+ (name) => name === ref.backend || config.backends[name].adapter === ref.backend
1546
+ );
1547
+ if (!backendName) {
1548
+ console.error(`No backend found matching "${ref.backend}".`);
1549
+ console.error(`Configured backends: ${Object.keys(config.backends).join(", ")}`);
1550
+ process.exit(1);
1551
+ }
1552
+ let task = findTaskByBackendId(backendName, ref.id);
1553
+ if (!task) {
1554
+ console.log(`Task #${ref.id} not in Taskwarrior yet, syncing ${backendName}...`);
1555
+ await syncBackend(backendName, config.backends[backendName], config);
1556
+ task = findTaskByBackendId(backendName, ref.id);
1557
+ }
1558
+ if (!task) {
1559
+ console.error(`Task #${ref.id} not found in backend "${backendName}" after sync.`);
1560
+ process.exit(1);
1561
+ }
1562
+ return task;
1563
+ }
1564
+ async function startCmd() {
1565
+ const ref = process.argv[3];
1566
+ if (!ref || ref.startsWith("--")) {
1567
+ console.error("Usage: tw-bridge start <backend>#<id> (e.g., tw-bridge start ghp#123)");
1568
+ process.exit(1);
1569
+ }
1570
+ const parsed = parseBackendRef(ref);
1571
+ if (!parsed) {
1572
+ console.error(`Invalid reference "${ref}". Expected format: backend#id (e.g., ghp#123)`);
1573
+ process.exit(1);
1574
+ }
1575
+ const task = await resolveTaskByRef(parsed);
1576
+ if (task.start) {
1577
+ console.log(`Task #${parsed.id} is already started (${task.uuid.slice(0, 8)})`);
1578
+ return;
1579
+ }
1580
+ if (startTask(task.uuid)) {
1581
+ console.log(`Started: [#${parsed.id}] ${task.description} (${task.uuid.slice(0, 8)})`);
1582
+ } else {
1583
+ console.error(`Failed to start task ${task.uuid}`);
1584
+ process.exit(1);
1585
+ }
1586
+ }
1587
+ async function doneCmd() {
1588
+ const ref = process.argv[3];
1589
+ if (!ref || ref.startsWith("--")) {
1590
+ console.error("Usage: tw-bridge done <backend>#<id> (e.g., tw-bridge done ghp#123)");
1591
+ process.exit(1);
1592
+ }
1593
+ const parsed = parseBackendRef(ref);
1594
+ if (!parsed) {
1595
+ console.error(`Invalid reference "${ref}". Expected format: backend#id (e.g., ghp#123)`);
1596
+ process.exit(1);
1597
+ }
1598
+ const task = await resolveTaskByRef(parsed);
1599
+ if (doneTask(task.uuid)) {
1600
+ console.log(`Completed: [#${parsed.id}] ${task.description} (${task.uuid.slice(0, 8)})`);
1601
+ } else {
1602
+ console.error(`Failed to complete task ${task.uuid}`);
1603
+ process.exit(1);
1604
+ }
1605
+ }
1196
1606
  async function syncBackend(name, backend, config) {
1197
1607
  const adapter = await resolveAdapter(
1198
1608
  { backend: name },
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/hooks/on-modify.ts
4
- import fs4 from "fs";
4
+ import fs5 from "fs";
5
5
  import { spawnSync as spawnSync3 } from "child_process";
6
6
 
7
7
  // src/config.ts
@@ -234,8 +234,8 @@ async function oauthLogin(clientId, clientSecret, rl) {
234
234
  saveTokens(tokens);
235
235
  return tokens;
236
236
  }
237
- async function asanaFetch(path5, token, options = {}) {
238
- const url = `${API_BASE}${path5}`;
237
+ async function asanaFetch(path6, token, options = {}) {
238
+ const url = `${API_BASE}${path6}`;
239
239
  const res = await fetch(url, {
240
240
  ...options,
241
241
  headers: {
@@ -251,9 +251,9 @@ async function asanaFetch(path5, token, options = {}) {
251
251
  const json = await res.json();
252
252
  return json.data;
253
253
  }
254
- async function asanaFetchAll(path5, token) {
254
+ async function asanaFetchAll(path6, token) {
255
255
  const results = [];
256
- let nextPage = path5;
256
+ let nextPage = path6;
257
257
  while (nextPage) {
258
258
  const url = `${API_BASE}${nextPage}`;
259
259
  const res = await fetch(url, {
@@ -565,10 +565,319 @@ function mapAsanaPriority(fields) {
565
565
  return void 0;
566
566
  }
567
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
+
568
876
  // src/registry.ts
569
877
  var BUILTIN_ADAPTERS = {
570
878
  ghp: () => new GhpAdapter(),
571
- asana: () => new AsanaAdapter()
879
+ asana: () => new AsanaAdapter(),
880
+ strety: () => new StretyAdapter()
572
881
  };
573
882
  function matchBackend(task, config) {
574
883
  if (task.backend && config.backends[task.backend]) {
@@ -600,21 +909,21 @@ async function resolveAdapter(task, config) {
600
909
  }
601
910
 
602
911
  // src/tracking.ts
603
- import fs3 from "fs";
604
- import path4 from "path";
605
- import os3 from "os";
912
+ import fs4 from "fs";
913
+ import path5 from "path";
914
+ import os4 from "os";
606
915
  import { spawnSync as spawnSync2 } from "child_process";
607
- var TRACKING_FILE = path4.join(os3.homedir(), ".config", "tw-bridge", "tracking.json");
916
+ var TRACKING_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", "tracking.json");
608
917
  function loadTracking() {
609
918
  try {
610
- return JSON.parse(fs3.readFileSync(TRACKING_FILE, "utf-8"));
919
+ return JSON.parse(fs4.readFileSync(TRACKING_FILE, "utf-8"));
611
920
  } catch {
612
921
  return {};
613
922
  }
614
923
  }
615
924
  function saveTracking(state) {
616
- fs3.mkdirSync(path4.dirname(TRACKING_FILE), { recursive: true });
617
- fs3.writeFileSync(TRACKING_FILE, JSON.stringify(state));
925
+ fs4.mkdirSync(path5.dirname(TRACKING_FILE), { recursive: true });
926
+ fs4.writeFileSync(TRACKING_FILE, JSON.stringify(state));
618
927
  }
619
928
  function mergedTags(state) {
620
929
  const all = /* @__PURE__ */ new Set();
@@ -689,10 +998,10 @@ tw-bridge: Currently tracking: ${tags} (${duration})`
689
998
  lines.push(" [s] Switch \u2014 stop current, start new task (default)");
690
999
  lines.push(" [p] Parallel \u2014 add new task to current tracking");
691
1000
  lines.push("");
692
- fs4.writeSync(ttyFd, lines.join("\n"));
693
- fs4.writeSync(ttyFd, " > ");
1001
+ fs5.writeSync(ttyFd, lines.join("\n"));
1002
+ fs5.writeSync(ttyFd, " > ");
694
1003
  const buf = Buffer.alloc(64);
695
- const bytesRead = fs4.readSync(ttyFd, buf, 0, 64, null);
1004
+ const bytesRead = fs5.readSync(ttyFd, buf, 0, 64, null);
696
1005
  const answer = buf.toString("utf-8", 0, bytesRead).trim().toLowerCase();
697
1006
  return answer.startsWith("p") ? "parallel" : "switch";
698
1007
  }
@@ -738,7 +1047,7 @@ function handleTimewarriorStop(config, task) {
738
1047
  }
739
1048
  }
740
1049
  async function main() {
741
- const input = fs4.readFileSync("/dev/stdin", "utf-8").trim().split("\n");
1050
+ const input = fs5.readFileSync("/dev/stdin", "utf-8").trim().split("\n");
742
1051
  const oldTask = JSON.parse(input[0]);
743
1052
  const newTask = JSON.parse(input[1]);
744
1053
  process.stdout.write(JSON.stringify(newTask) + "\n");
@@ -749,7 +1058,7 @@ async function main() {
749
1058
  let ttyFd = null;
750
1059
  if (wasStarted) {
751
1060
  try {
752
- ttyFd = fs4.openSync("/dev/tty", "r+");
1061
+ ttyFd = fs5.openSync("/dev/tty", "r+");
753
1062
  } catch {
754
1063
  }
755
1064
  }
@@ -759,6 +1068,7 @@ async function main() {
759
1068
  } else if (wasStopped || wasCompleted) {
760
1069
  handleTimewarriorStop(config, oldTask);
761
1070
  }
1071
+ if (process.env.TW_BRIDGE_REVERSE_SYNC) return;
762
1072
  const adapter = await resolveAdapter(newTask, config);
763
1073
  if (!adapter) return;
764
1074
  if (wasStarted && adapter.onStart) {
@@ -777,7 +1087,7 @@ async function main() {
777
1087
  process.stderr.write(`tw-bridge: ${msg}
778
1088
  `);
779
1089
  } finally {
780
- if (ttyFd !== null) fs4.closeSync(ttyFd);
1090
+ if (ttyFd !== null) fs5.closeSync(ttyFd);
781
1091
  }
782
1092
  }
783
1093
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bretwardjames/tw-bridge",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Taskwarrior backend bridge — unified sync and hooks for multiple task management platforms",
5
5
  "type": "module",
6
6
  "license": "MIT",