@thesammykins/tether 1.0.1 → 1.3.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/.env.example +14 -0
- package/README.md +25 -283
- package/bin/tether.ts +398 -32
- package/docs/agents.md +205 -0
- package/docs/architecture.md +117 -0
- package/docs/cli.md +277 -0
- package/docs/configuration.md +160 -0
- package/docs/discord-setup.md +122 -0
- package/docs/installation.md +151 -0
- package/docs/troubleshooting.md +74 -0
- package/package.json +2 -1
- package/src/api.ts +89 -6
- package/src/bot.ts +114 -31
- package/src/config.ts +392 -0
- package/src/db.ts +6 -0
- package/src/features/pause-resume.ts +3 -3
- package/src/queue.ts +1 -0
- package/src/worker.ts +14 -2
- package/src/spawner.ts +0 -110
package/bin/tether.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* tether status - Show running status
|
|
9
9
|
* tether health - Check Distether connection
|
|
10
10
|
* tether setup - Interactive setup wizard
|
|
11
|
+
* tether config - Manage configuration and encrypted secrets
|
|
11
12
|
*
|
|
12
13
|
* Distether Commands:
|
|
13
14
|
* tether send <channel> "message"
|
|
@@ -31,11 +32,17 @@
|
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
import { spawn, spawnSync } from 'bun';
|
|
34
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
|
|
35
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, symlinkSync, lstatSync, readlinkSync, unlinkSync } from 'fs';
|
|
35
36
|
import { join, dirname } from 'path';
|
|
36
37
|
import * as readline from 'readline';
|
|
37
38
|
import { homedir } from 'os';
|
|
38
39
|
import { randomUUID } from 'crypto';
|
|
40
|
+
import {
|
|
41
|
+
ensureConfigDir, readPreferences, writePreference, readSecrets, writeSecret,
|
|
42
|
+
deleteKey as deleteConfigKey, resolve as resolveConfig, resolveAll,
|
|
43
|
+
isKnownKey, isSecret, getKeyMeta, getKnownKeys, hasSecrets, hasConfig,
|
|
44
|
+
importDotEnv, CONFIG_PATHS,
|
|
45
|
+
} from '../src/config';
|
|
39
46
|
|
|
40
47
|
const PID_FILE = join(process.cwd(), '.tether.pid');
|
|
41
48
|
const API_BASE = process.env.TETHER_API_URL || 'http://localhost:2643';
|
|
@@ -80,9 +87,16 @@ async function prompt(question: string): Promise<string> {
|
|
|
80
87
|
|
|
81
88
|
async function apiCall(endpoint: string, body: any): Promise<any> {
|
|
82
89
|
try {
|
|
90
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
91
|
+
|
|
92
|
+
// Add Authorization header if API_TOKEN is set
|
|
93
|
+
if (process.env.API_TOKEN) {
|
|
94
|
+
headers['Authorization'] = `Bearer ${process.env.API_TOKEN}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
84
98
|
method: 'POST',
|
|
85
|
-
headers
|
|
99
|
+
headers,
|
|
86
100
|
body: JSON.stringify(body),
|
|
87
101
|
});
|
|
88
102
|
const data = await response.json();
|
|
@@ -689,28 +703,106 @@ async function setup() {
|
|
|
689
703
|
console.log('⚠ Redis not running. Start it with: redis-server');
|
|
690
704
|
}
|
|
691
705
|
|
|
692
|
-
// Check
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
706
|
+
// Check Agent CLI based on TETHER_AGENT env var
|
|
707
|
+
const agentType = process.env.TETHER_AGENT || 'claude';
|
|
708
|
+
let agentBinary: string;
|
|
709
|
+
let agentInstallUrl: string;
|
|
710
|
+
|
|
711
|
+
switch (agentType) {
|
|
712
|
+
case 'claude-code':
|
|
713
|
+
case 'claude':
|
|
714
|
+
agentBinary = 'claude';
|
|
715
|
+
agentInstallUrl = 'https://claude.ai/code';
|
|
716
|
+
break;
|
|
717
|
+
case 'opencode':
|
|
718
|
+
agentBinary = 'opencode';
|
|
719
|
+
agentInstallUrl = 'https://github.com/getcursor/opencode';
|
|
720
|
+
break;
|
|
721
|
+
case 'codex-cli':
|
|
722
|
+
case 'codex':
|
|
723
|
+
agentBinary = 'codex';
|
|
724
|
+
agentInstallUrl = 'https://github.com/getcursor/codex';
|
|
725
|
+
break;
|
|
726
|
+
default:
|
|
727
|
+
agentBinary = agentType;
|
|
728
|
+
agentInstallUrl = '(unknown agent type)';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const agentCheck = spawnSync([agentBinary, '--version'], { stdout: 'pipe', stderr: 'pipe' });
|
|
732
|
+
if (agentCheck.exitCode === 0) {
|
|
733
|
+
console.log(`✓ ${agentBinary} CLI installed`);
|
|
696
734
|
} else {
|
|
697
|
-
console.log(
|
|
735
|
+
console.log(`⚠ ${agentBinary} CLI not found. Install from: ${agentInstallUrl}`);
|
|
698
736
|
}
|
|
699
737
|
|
|
700
|
-
// Install Claude Code
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
const sourceSkillsDir = join(
|
|
738
|
+
// Install Tether skill for agent (legacy Claude Code location)
|
|
739
|
+
const legacySkillsDir = join(homedir(), '.claude', 'skills', 'tether');
|
|
740
|
+
const tetherRoot = dirname(import.meta.dir);
|
|
741
|
+
const sourceSkillsDir = join(tetherRoot, 'skills', 'tether');
|
|
704
742
|
|
|
705
743
|
if (existsSync(sourceSkillsDir)) {
|
|
706
|
-
console.log('\n📚 Claude Code
|
|
707
|
-
console.log(' Teaches your assistant how to send
|
|
744
|
+
console.log('\n📚 Tether Skill (Legacy Claude Code location)');
|
|
745
|
+
console.log(' Teaches your assistant how to send Discord messages, embeds,');
|
|
708
746
|
console.log(' files, and interactive buttons.');
|
|
709
|
-
const installSkill = await prompt('Install skill? (Y/n): ');
|
|
747
|
+
const installSkill = await prompt('Install skill to ~/.claude/skills/tether? (Y/n): ');
|
|
710
748
|
if (installSkill.toLowerCase() !== 'n') {
|
|
711
|
-
mkdirSync(
|
|
712
|
-
cpSync(sourceSkillsDir,
|
|
713
|
-
console.log(`✓ Skill installed to ${
|
|
749
|
+
mkdirSync(legacySkillsDir, { recursive: true });
|
|
750
|
+
cpSync(sourceSkillsDir, legacySkillsDir, { recursive: true });
|
|
751
|
+
console.log(`✓ Skill installed to ${legacySkillsDir}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Symlink skill to standard location (~/.agents/skills/tether)
|
|
756
|
+
const standardSkillsDir = join(homedir(), '.agents', 'skills', 'tether');
|
|
757
|
+
|
|
758
|
+
if (existsSync(sourceSkillsDir)) {
|
|
759
|
+
console.log('\n📚 Tether Skill (Standard location for all agents)');
|
|
760
|
+
console.log(' Symlinking to ~/.agents/skills/tether for OpenCode, Codex, etc.');
|
|
761
|
+
|
|
762
|
+
// Create ~/.agents/skills/ if it doesn't exist
|
|
763
|
+
mkdirSync(join(homedir(), '.agents', 'skills'), { recursive: true });
|
|
764
|
+
|
|
765
|
+
// Check if symlink already exists
|
|
766
|
+
let shouldCreateSymlink = true;
|
|
767
|
+
if (existsSync(standardSkillsDir)) {
|
|
768
|
+
try {
|
|
769
|
+
const stats = lstatSync(standardSkillsDir);
|
|
770
|
+
if (stats.isSymbolicLink()) {
|
|
771
|
+
const target = readlinkSync(standardSkillsDir);
|
|
772
|
+
if (target === sourceSkillsDir) {
|
|
773
|
+
console.log(`✓ Symlink already exists and points to correct location`);
|
|
774
|
+
shouldCreateSymlink = false;
|
|
775
|
+
} else {
|
|
776
|
+
console.log(`⚠ Symlink exists but points to: ${target}`);
|
|
777
|
+
const overwrite = await prompt('Overwrite? (Y/n): ');
|
|
778
|
+
if (overwrite.toLowerCase() !== 'n') {
|
|
779
|
+
unlinkSync(standardSkillsDir);
|
|
780
|
+
} else {
|
|
781
|
+
shouldCreateSymlink = false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} else {
|
|
785
|
+
console.log(`⚠ ${standardSkillsDir} exists but is not a symlink`);
|
|
786
|
+
const overwrite = await prompt('Remove and create symlink? (Y/n): ');
|
|
787
|
+
if (overwrite.toLowerCase() !== 'n') {
|
|
788
|
+
unlinkSync(standardSkillsDir);
|
|
789
|
+
} else {
|
|
790
|
+
shouldCreateSymlink = false;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
} catch (err) {
|
|
794
|
+
console.log(`⚠ Error checking existing path: ${err}`);
|
|
795
|
+
shouldCreateSymlink = false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (shouldCreateSymlink) {
|
|
800
|
+
try {
|
|
801
|
+
symlinkSync(sourceSkillsDir, standardSkillsDir, 'dir');
|
|
802
|
+
console.log(`✓ Symlinked ${standardSkillsDir} → ${sourceSkillsDir}`);
|
|
803
|
+
} catch (err) {
|
|
804
|
+
console.log(`⚠ Failed to create symlink: ${err}`);
|
|
805
|
+
}
|
|
714
806
|
}
|
|
715
807
|
}
|
|
716
808
|
|
|
@@ -725,18 +817,46 @@ async function start() {
|
|
|
725
817
|
|
|
726
818
|
console.log('Starting Tether...\n');
|
|
727
819
|
|
|
820
|
+
// Resolve script paths relative to the package root, not process.cwd().
|
|
821
|
+
// When installed globally or via npx, cwd is the user's project dir where
|
|
822
|
+
// src/bot.ts doesn't exist. import.meta.dir is bin/, so one level up is root.
|
|
823
|
+
const packageRoot = dirname(import.meta.dir);
|
|
824
|
+
const botScript = join(packageRoot, 'src', 'bot.ts');
|
|
825
|
+
const workerScript = join(packageRoot, 'src', 'worker.ts');
|
|
826
|
+
|
|
827
|
+
// Load config store values into child process environment.
|
|
828
|
+
// Secrets are encrypted on disk — decrypt them so bot/worker can read
|
|
829
|
+
// DISCORD_BOT_TOKEN etc. from process.env as they expect.
|
|
830
|
+
const childEnv: Record<string, string | undefined> = { ...process.env };
|
|
831
|
+
const prefs = readPreferences();
|
|
832
|
+
for (const [key, value] of Object.entries(prefs)) {
|
|
833
|
+
if (!childEnv[key]) childEnv[key] = value;
|
|
834
|
+
}
|
|
835
|
+
if (hasSecrets()) {
|
|
836
|
+
const pw = await promptPassword('Encryption password: ');
|
|
837
|
+
try {
|
|
838
|
+
const secrets = readSecrets(pw);
|
|
839
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
840
|
+
if (!childEnv[key]) childEnv[key] = value;
|
|
841
|
+
}
|
|
842
|
+
} catch {
|
|
843
|
+
console.error('Wrong password or corrupted secrets file.');
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
728
848
|
// Start bot
|
|
729
|
-
const bot = spawn(['bun', 'run',
|
|
849
|
+
const bot = spawn(['bun', 'run', botScript], {
|
|
730
850
|
stdout: 'inherit',
|
|
731
851
|
stderr: 'inherit',
|
|
732
|
-
|
|
852
|
+
env: childEnv,
|
|
733
853
|
});
|
|
734
854
|
|
|
735
855
|
// Start worker
|
|
736
|
-
const worker = spawn(['bun', 'run',
|
|
856
|
+
const worker = spawn(['bun', 'run', workerScript], {
|
|
737
857
|
stdout: 'inherit',
|
|
738
858
|
stderr: 'inherit',
|
|
739
|
-
|
|
859
|
+
env: childEnv,
|
|
740
860
|
});
|
|
741
861
|
|
|
742
862
|
// Save PIDs
|
|
@@ -834,6 +954,236 @@ async function health() {
|
|
|
834
954
|
}
|
|
835
955
|
}
|
|
836
956
|
|
|
957
|
+
// ============ Config ============
|
|
958
|
+
|
|
959
|
+
async function promptPassword(label = 'Password: '): Promise<string> {
|
|
960
|
+
// Use raw mode to hide password input
|
|
961
|
+
if (process.stdin.isTTY) {
|
|
962
|
+
process.stdout.write(label);
|
|
963
|
+
return new Promise((resolve) => {
|
|
964
|
+
let pw = '';
|
|
965
|
+
process.stdin.setRawMode(true);
|
|
966
|
+
process.stdin.resume();
|
|
967
|
+
process.stdin.setEncoding('utf-8');
|
|
968
|
+
process.stdin.on('data', (ch: string) => {
|
|
969
|
+
if (ch === '\r' || ch === '\n') {
|
|
970
|
+
process.stdin.setRawMode(false);
|
|
971
|
+
process.stdin.pause();
|
|
972
|
+
process.stdout.write('\n');
|
|
973
|
+
resolve(pw);
|
|
974
|
+
} else if (ch === '\x03') {
|
|
975
|
+
process.exit(130);
|
|
976
|
+
} else if (ch === '\x7f' || ch === '\b') {
|
|
977
|
+
pw = pw.slice(0, -1);
|
|
978
|
+
} else {
|
|
979
|
+
pw += ch;
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
return prompt(label);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async function configCommand() {
|
|
988
|
+
const subcommand = args[0];
|
|
989
|
+
|
|
990
|
+
switch (subcommand) {
|
|
991
|
+
case 'set': {
|
|
992
|
+
const key = args[1];
|
|
993
|
+
if (!key) {
|
|
994
|
+
console.error('Usage: tether config set <key> [value]');
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
if (!isKnownKey(key)) {
|
|
998
|
+
console.error(`Unknown config key: ${key}`);
|
|
999
|
+
console.error(`Run "tether config list" to see all keys`);
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
let value = args[2];
|
|
1004
|
+
if (isSecret(key)) {
|
|
1005
|
+
if (!value) {
|
|
1006
|
+
value = await promptPassword(`${key}: `);
|
|
1007
|
+
}
|
|
1008
|
+
const pw = await promptPassword('Encryption password: ');
|
|
1009
|
+
if (!pw) {
|
|
1010
|
+
console.error('Password cannot be empty');
|
|
1011
|
+
process.exit(1);
|
|
1012
|
+
}
|
|
1013
|
+
writeSecret(key, value, pw);
|
|
1014
|
+
console.log(`✔ Secret "${key}" saved (encrypted)`);
|
|
1015
|
+
} else {
|
|
1016
|
+
if (value === undefined) {
|
|
1017
|
+
console.error(`Usage: tether config set ${key} <value>`);
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
writePreference(key, value);
|
|
1021
|
+
console.log(`✔ "${key}" = "${value}"`);
|
|
1022
|
+
}
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
case 'get': {
|
|
1027
|
+
const key = args[1];
|
|
1028
|
+
if (!key) {
|
|
1029
|
+
console.error('Usage: tether config get <key>');
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
if (!isKnownKey(key)) {
|
|
1033
|
+
console.error(`Unknown config key: ${key}`);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
let password: string | undefined;
|
|
1038
|
+
if (isSecret(key) && hasSecrets()) {
|
|
1039
|
+
password = await promptPassword('Encryption password: ');
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const value = resolveConfig(key, password);
|
|
1043
|
+
const meta = getKeyMeta(key);
|
|
1044
|
+
|
|
1045
|
+
// Show source
|
|
1046
|
+
const envValue = process.env[key];
|
|
1047
|
+
let source = 'default';
|
|
1048
|
+
if (envValue !== undefined && envValue !== '') {
|
|
1049
|
+
source = 'env';
|
|
1050
|
+
} else if (isSecret(key) && password) {
|
|
1051
|
+
try {
|
|
1052
|
+
const secrets = readSecrets(password);
|
|
1053
|
+
if (key in secrets) source = 'secrets.enc';
|
|
1054
|
+
} catch { /* wrong password */ }
|
|
1055
|
+
} else {
|
|
1056
|
+
const prefs = readPreferences();
|
|
1057
|
+
if (key in prefs) source = 'config.toml';
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
console.log(`${key} = ${value || '(empty)'}`);
|
|
1061
|
+
console.log(` source: ${source} section: [${meta?.section}]`);
|
|
1062
|
+
if (meta?.description) console.log(` ${meta.description}`);
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
case 'list': {
|
|
1067
|
+
const keys = getKnownKeys();
|
|
1068
|
+
const prefs = readPreferences();
|
|
1069
|
+
|
|
1070
|
+
console.log('\nTether Configuration\n');
|
|
1071
|
+
let currentSection = '';
|
|
1072
|
+
|
|
1073
|
+
for (const key of keys) {
|
|
1074
|
+
const meta = getKeyMeta(key)!;
|
|
1075
|
+
if (meta.section !== currentSection) {
|
|
1076
|
+
currentSection = meta.section;
|
|
1077
|
+
console.log(`[${currentSection}]`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Determine value & source
|
|
1081
|
+
const envValue = process.env[key];
|
|
1082
|
+
let value: string;
|
|
1083
|
+
let source: string;
|
|
1084
|
+
|
|
1085
|
+
if (envValue !== undefined && envValue !== '') {
|
|
1086
|
+
value = isSecret(key) ? '***' : envValue;
|
|
1087
|
+
source = 'env';
|
|
1088
|
+
} else if (isSecret(key)) {
|
|
1089
|
+
value = hasSecrets() ? '(encrypted)' : '(not set)';
|
|
1090
|
+
source = hasSecrets() ? 'secrets.enc' : 'default';
|
|
1091
|
+
} else if (key in prefs) {
|
|
1092
|
+
value = prefs[key]!;
|
|
1093
|
+
source = 'config.toml';
|
|
1094
|
+
} else {
|
|
1095
|
+
value = meta.default || '(not set)';
|
|
1096
|
+
source = 'default';
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const pad = ' '.repeat(Math.max(1, 28 - key.length));
|
|
1100
|
+
console.log(` ${key}${pad}${value} (${source})`);
|
|
1101
|
+
}
|
|
1102
|
+
console.log('');
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
case 'delete':
|
|
1107
|
+
case 'unset': {
|
|
1108
|
+
const key = args[1];
|
|
1109
|
+
if (!key) {
|
|
1110
|
+
console.error('Usage: tether config delete <key>');
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
if (!isKnownKey(key)) {
|
|
1114
|
+
console.error(`Unknown config key: ${key}`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
let password: string | undefined;
|
|
1119
|
+
if (isSecret(key)) {
|
|
1120
|
+
password = await promptPassword('Encryption password: ');
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const deleted = deleteConfigKey(key, password);
|
|
1124
|
+
if (deleted) {
|
|
1125
|
+
console.log(`✔ "${key}" deleted`);
|
|
1126
|
+
} else {
|
|
1127
|
+
console.log(`"${key}" was not set`);
|
|
1128
|
+
}
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
case 'import': {
|
|
1133
|
+
const envPath = args[1] || join(process.cwd(), '.env');
|
|
1134
|
+
if (!existsSync(envPath)) {
|
|
1135
|
+
console.error(`File not found: ${envPath}`);
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const pw = await promptPassword('Encryption password (for secrets): ');
|
|
1140
|
+
if (!pw) {
|
|
1141
|
+
console.error('Password cannot be empty');
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const result = importDotEnv(envPath, pw);
|
|
1146
|
+
console.log(`\n✔ Imported ${result.imported.length} keys:`);
|
|
1147
|
+
for (const k of result.imported) {
|
|
1148
|
+
console.log(` ${k}${isSecret(k) ? ' (encrypted)' : ''}`);
|
|
1149
|
+
}
|
|
1150
|
+
if (result.skipped.length > 0) {
|
|
1151
|
+
console.log(`\n⚠ Skipped ${result.skipped.length} keys:`);
|
|
1152
|
+
for (const k of result.skipped) {
|
|
1153
|
+
console.log(` ${k}${isKnownKey(k) ? ' (empty/placeholder)' : ' (unknown)'}`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
console.log('');
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
case 'path': {
|
|
1161
|
+
console.log(`Config dir: ${CONFIG_PATHS.CONFIG_DIR}`);
|
|
1162
|
+
console.log(`Preferences: ${CONFIG_PATHS.CONFIG_PATH} ${hasConfig() ? '✔' : '(not created)'}`);
|
|
1163
|
+
console.log(`Secrets: ${CONFIG_PATHS.SECRETS_PATH} ${hasSecrets() ? '✔' : '(not created)'}`);
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
default:
|
|
1168
|
+
console.log(`
|
|
1169
|
+
Usage: tether config <subcommand>
|
|
1170
|
+
|
|
1171
|
+
Subcommands:
|
|
1172
|
+
set <key> [value] Set a config value (prompts for secrets)
|
|
1173
|
+
get <key> Get a resolved config value
|
|
1174
|
+
list List all config values with sources
|
|
1175
|
+
delete <key> Delete a config value
|
|
1176
|
+
import [path] Import from .env file (default: ./.env)
|
|
1177
|
+
path Show config file locations
|
|
1178
|
+
`);
|
|
1179
|
+
if (subcommand) {
|
|
1180
|
+
console.error(`Unknown config subcommand: ${subcommand}`);
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
837
1187
|
function showHelp() {
|
|
838
1188
|
console.log(`
|
|
839
1189
|
Tether - Distether to Claude Code bridge
|
|
@@ -846,6 +1196,7 @@ Management Commands:
|
|
|
846
1196
|
status Show running status
|
|
847
1197
|
health Check Distether connection
|
|
848
1198
|
setup Interactive setup wizard
|
|
1199
|
+
config Manage configuration and encrypted secrets
|
|
849
1200
|
help Show this help
|
|
850
1201
|
|
|
851
1202
|
Distether Commands:
|
|
@@ -916,20 +1267,32 @@ DM Commands (proactive outreach):
|
|
|
916
1267
|
dm <user-id> --embed "description" [options]
|
|
917
1268
|
Send an embed DM (same options as embed command)
|
|
918
1269
|
|
|
919
|
-
|
|
920
|
-
|
|
1270
|
+
dm <user-id> --file <filepath> ["message"]
|
|
1271
|
+
Send a file attachment via DM
|
|
1272
|
+
|
|
1273
|
+
Config Commands:
|
|
1274
|
+
config set <key> [value] Set a config value (prompts for secrets)
|
|
1275
|
+
config get <key> Get a resolved config value with source
|
|
1276
|
+
config list List all config values with sources
|
|
1277
|
+
config delete <key> Delete a config value
|
|
1278
|
+
config import [path] Import from .env file (default: ./.env)
|
|
1279
|
+
config path Show config file locations
|
|
921
1280
|
|
|
922
1281
|
Examples:
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1282
|
+
tether send 123456789 "Hello world!"
|
|
1283
|
+
tether embed 123456789 "Status update" --title "Daily Report" --color green --field "Tasks:5 done:inline"
|
|
1284
|
+
tether buttons 123456789 "Approve?" --button label="Yes" id="approve" style="success" reply="Approved!"
|
|
1285
|
+
tether ask 123456789 "Deploy to prod?" --option "Yes" --option "No" --timeout 600
|
|
1286
|
+
tether file 123456789 ./report.md "Here's the report"
|
|
1287
|
+
tether state 123456789 1234567890 processing
|
|
929
1288
|
tether state 123456789 1234567890 done
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1289
|
+
tether dm 987654321 "Hey, I need your approval on this PR"
|
|
1290
|
+
tether dm 987654321 --embed "Build passed" --title "CI Update" --color green
|
|
1291
|
+
tether dm 987654321 --file ./report.md "Here's the report"
|
|
1292
|
+
tether config set AGENT_TYPE opencode
|
|
1293
|
+
tether config set DISCORD_BOT_TOKEN
|
|
1294
|
+
tether config import .env
|
|
1295
|
+
tether config list
|
|
933
1296
|
`);
|
|
934
1297
|
}
|
|
935
1298
|
|
|
@@ -1002,6 +1365,9 @@ switch (command) {
|
|
|
1002
1365
|
case 'dm':
|
|
1003
1366
|
sendDM();
|
|
1004
1367
|
break;
|
|
1368
|
+
case 'config':
|
|
1369
|
+
configCommand();
|
|
1370
|
+
break;
|
|
1005
1371
|
|
|
1006
1372
|
default:
|
|
1007
1373
|
console.log(`Unknown command: ${command}`);
|