···21212222## Git SSH Key Management
23232424-- [ ] Implement `tangled ssh-key add <public-key-path>` command.
2525- - [ ] This command should upload the provided public SSH key to the user's tangled.org account via the API, similar to how `gh ssh-key add` works. If no path is provided, it should default to `~/.ssh/id_rsa.pub` or prompt the user for a path.
2626- - [ ] The CLI is not responsible for generating SSH keys or managing the local ssh-agent; users are expected to handle these steps externally.
2727-- [ ] Implement `tangled ssh-key verify` command.
2828- - [ ] This command should execute `ssh -T git@tangled.org`, parse the DID from its output, and then resolve that DID to a Bluesky handle, displaying the result to the user.
2929-- [ ] Ensure all Git operations leverage SSH keys for authentication, as `tangled.org` exclusively supports SSH for Git.
2424+- [x] Implement `tangled ssh-key verify` command.
2525+ - [x] This command executes `ssh -T git@tangled.org`, parses the DID from its output, and displays it to the user.
2626+ - [x] If the user is logged in with the CLI and their DID matches the SSH DID, their handle is also displayed.
30273128## Context Engine (Git Integration)
3229···78757976- [ ] This phase primarily involves local Git operations (pushing new commits) and using `tangled pr comment` for clarifications, which are covered by existing or planned commands.
80777878+## SSH Key Upload & Management (Phase 4)
7979+8080+This phase adds CLI-based SSH key management for users who want to upload keys programmatically.
8181+8282+- [ ] Implement `tangled ssh-key add <public-key-path>` command.
8383+ - [ ] This command should upload the provided public SSH key to the user's tangled.org account via the API, similar to how `gh ssh-key add` works. If no path is provided, it should default to `~/.ssh/id_ed25519.pub` or prompt the user for a path.
8484+ - [ ] Support reading keys from SSH agent via `ssh-add -L` for 1Password SSH agent users.
8585+ - [ ] The CLI is not responsible for generating SSH keys or managing the local ssh-agent; users are expected to handle these steps externally.
8686+- [ ] Implement `tangled ssh-key list` command.
8787+ - [ ] List all SSH keys stored in the user's PDS.
8888+ - [ ] Display key type, name, creation date, and URI.
8989+8190## Output & LLM Integration
82918392- [ ] Implement output formatting based on `is-interactive` check.
···88978998## Testing
90999191-- [ ] Set up a testing framework (e.g., Jest, Vitest).
9292-- [ ] Write unit tests for core modules (Auth, Context Resolver, API client).
9393-- [ ] Write integration tests for CLI commands.
100100+- [x] Set up a testing framework (Vitest).
101101+- [x] Write unit tests for core modules (Auth, Session, API client, Validation, Prompts).
102102+- [x] Write integration tests for CLI commands (Auth, SSH key verify).
103103+- [ ] Add integration tests for remaining commands as they are implemented.
9410495105## Documentation & Deployment
96106···99109100110## Outstanding Issues / Future Considerations (from README)
101111102102-- [ ] Secure cross-platform AT Proto session storage (OS keychain).
103103-- [ ] Git authentication management similar to GitHub CLI (SSH keys, 1Password integration).
112112+- [x] Secure cross-platform AT Proto session storage (OS keychain) - Implemented with @napi-rs/keyring.
113113+- [x] SSH key verification for Git authentication - Implemented `tangled ssh-key verify`.
114114+- [ ] SSH key upload management (See Phase 4 above).
115115+- [ ] 1Password SSH agent integration for key upload (See Phase 4 above).
104116- [ ] Define clear precedence order for settings resolution (local config, home folder, CLI flags).
105117- [ ] Consider adding extensions/plugins (Out of Scope for V1, but keep in mind).
+75
src/commands/ssh-key.ts
···11+import { execSync } from 'node:child_process';
22+import { Command } from 'commander';
33+import { getCurrentSessionMetadata } from '../lib/session.js';
44+55+/**
66+ * Create the ssh-key command with subcommands for managing SSH keys
77+ */
88+export function createSshKeyCommand(): Command {
99+ const sshKey = new Command('ssh-key');
1010+ sshKey.description('Verify SSH key setup for Git authentication');
1111+1212+ // Verify command
1313+ sshKey
1414+ .command('verify')
1515+ .description('Verify SSH key authentication with git@tangled.org')
1616+ .action(async () => {
1717+ try {
1818+ console.log('Testing SSH connection to git@tangled.org...\n');
1919+2020+ // Execute ssh -T git@tangled.org to test authentication
2121+ let output: string;
2222+ try {
2323+ output = execSync('ssh -T git@tangled.org', {
2424+ encoding: 'utf-8',
2525+ stdio: 'pipe',
2626+ });
2727+ } catch (error) {
2828+ // ssh -T returns non-zero exit code even on success
2929+ // Capture stderr which contains the authentication message
3030+ if (error instanceof Error && 'stderr' in error) {
3131+ output = (error as { stderr: string }).stderr;
3232+ } else {
3333+ throw error;
3434+ }
3535+ }
3636+3737+ // Parse the DID from the output
3838+ // Expected format: "Hi @did:plc:...! You've successfully authenticated."
3939+ const didMatch = output.match(/@(did:plc:[a-z0-9]+)/i);
4040+4141+ if (!didMatch) {
4242+ console.error('✗ SSH authentication failed');
4343+ console.error('Could not find authenticated DID in response');
4444+ console.error('\nPlease ensure you have:');
4545+ console.error('1. Generated an SSH key (ssh-keygen -t ed25519)');
4646+ console.error('2. Added your public key to https://tangled.org/settings/keys');
4747+ console.error('3. Your SSH agent is running (ssh-add -l)');
4848+ process.exit(1);
4949+ }
5050+5151+ const did = didMatch[1];
5252+ console.log('✓ SSH authentication successful');
5353+ console.log(` Authenticated as: ${did}`);
5454+5555+ // Check if this matches the logged-in user
5656+ const session = await getCurrentSessionMetadata();
5757+ if (session && session.did === did) {
5858+ console.log(` Handle: @${session.handle}`);
5959+ }
6060+6161+ console.log('\n✓ Your SSH setup is working correctly!');
6262+ } catch (error) {
6363+ console.error(
6464+ `\n✗ Failed to verify SSH setup: ${error instanceof Error ? error.message : 'Unknown error'}`
6565+ );
6666+ console.error('\nPlease ensure you have:');
6767+ console.error('1. Generated an SSH key (ssh-keygen -t ed25519)');
6868+ console.error('2. Added your public key to https://tangled.org/settings/keys');
6969+ console.error('3. Your SSH agent is running (ssh-add -l)');
7070+ process.exit(1);
7171+ }
7272+ });
7373+7474+ return sshKey;
7575+}
+2
src/index.ts
···44import { fileURLToPath } from 'node:url';
55import { Command } from 'commander';
66import { createAuthCommand } from './commands/auth.js';
77+import { createSshKeyCommand } from './commands/ssh-key.js';
7889// Get package.json for version
910const __filename = fileURLToPath(import.meta.url);
···19202021// Register commands
2122program.addCommand(createAuthCommand());
2323+program.addCommand(createSshKeyCommand());
22242325program.parse(process.argv);